Commit 39fae3ac authored by henrik.prangel's avatar henrik.prangel
Browse files

JUT-86 Add client metadata to client support conversation view

* Display last known message origin of client in conversation
* Move functions into services that are better suited for them
* Rename client support header title
parent 6ea71563
......@@ -67,6 +67,9 @@ public class Chat implements Serializable {
@Column(name = "has_been_labeled")
private Boolean hasBeenLabeled;
@Column(name = "last_user_referer")
private String lastUserReferer;
// jhipster-needle-entity-add-field - JHipster will add fields here
public Long getId() {
return id;
......@@ -242,6 +245,14 @@ public class Chat implements Serializable {
this.hasBeenLabeled = hasBeenLabeled;
}
public String getLastUserReferer() {
return lastUserReferer;
}
public void setLastUserReferer(String lastUserReferer) {
this.lastUserReferer = lastUserReferer;
}
// jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here
@Override
......
......@@ -6,9 +6,11 @@ import com.netgroup.riigibot.repository.MessageRepository;
import com.netgroup.riigibot.service.dto.ChatDTO;
import com.netgroup.riigibot.service.dto.MessageDTO;
import com.netgroup.riigibot.service.mapper.ChatMapper;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -109,21 +111,41 @@ public class ChatService {
return chatDTO;
}
public ChatDTO getOrCreateChatByMessage(MessageDTO messageDTO) {
ChatDTO messageChatDTO;
if (messageDTO.getChatId() == null) {
ChatDTO newChat = new ChatDTO();
newChat.setSessionId(messageDTO.getSessionId());
messageChatDTO = save(newChat);
messageDTO.setChatId(messageChatDTO.getId());
} else {
messageChatDTO = findOne(messageDTO.getChatId()).orElseGet(() -> {
log.error("Error finding chat with id:" + messageDTO.getChatId());
return null;
});
public boolean isChatHandledByCustomerSupport(ChatDTO chatDTO) {
return chatDTO.getCommittedAdmin() != null || (chatDTO.getNeedsCustomerService() != null && chatDTO.getNeedsCustomerService());
}
public ChatDTO createNewChatByMessage(MessageDTO messageDTO) {
ChatDTO newChat = new ChatDTO();
newChat.setSessionId(messageDTO.getSessionId());
return save(newChat);
}
public ChatDTO getChatByMessage(MessageDTO messageDTO) {
return findOne(messageDTO.getChatId()).orElseThrow(NoSuchElementException::new);
}
public void updateChatWithMessageInformation(ChatDTO chatDTO, MessageDTO messageDTO) {
chatDTO.setLastRespondedBot(messageDTO.getSender());
if (messageDTO.getText().equals("/restart")) {
chatDTO.setCommittedBot(null);
}
if (messageDTO.getSender() == null) {
chatDTO.setLastMessageId(messageDTO.getId());
}
save(chatDTO);
}
public void commitBotToChat(MessageDTO messageDTO, ChatDTO chatDTO) {
log.debug("Committing responder {} to chat {}", messageDTO.getSender(), chatDTO.getId());
chatDTO.setCommittedBot(messageDTO.getSender());
save(chatDTO);
}
return messageChatDTO;
public void commitAdminToChat(ChatDTO chatDTO, Principal principal) {
log.debug("Committing admin {} to chat {}", principal.getName(), chatDTO.getId());
chatDTO.setCommittedAdmin(principal.getName());
save(chatDTO);
}
}
......@@ -4,7 +4,6 @@ import com.netgroup.riigibot.service.dto.ChatDTO;
import com.netgroup.riigibot.service.dto.MessageDTO;
import com.netgroup.riigibot.service.enums.UnremarkableIntent;
import com.netgroup.riigibot.web.websocket.ActivityService;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
......@@ -12,22 +11,18 @@ import java.util.NoSuchElementException;
import org.apache.commons.lang3.EnumUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Controller;
@Controller
public class ConversationService {
private static final Logger log = LoggerFactory.getLogger(ActivityService.class);
private final SimpMessageSendingOperations messagingTemplate;
private final ChatService chatService;
private final MessageService messageService;
private final List<ChatMessageCollector> chatMessageCollectors = new ArrayList<>();
private static final Integer BOT_AMOUNT = 2;
public ConversationService(SimpMessageSendingOperations messagingTemplate, ChatService chatService, MessageService messageService) {
this.messagingTemplate = messagingTemplate;
public ConversationService(ChatService chatService, MessageService messageService) {
this.chatService = chatService;
this.messageService = messageService;
}
......@@ -42,22 +37,22 @@ public class ConversationService {
}
public boolean shouldCollectMessage(MessageDTO messageDTO) {
ChatDTO chat = chatService.getOrCreateChatByMessage(messageDTO);
ChatDTO chat = chatService.getChatByMessage(messageDTO);
return chat != null && (getChatCollectorByChatId(messageDTO.getChatId()) != null || chat.getCommittedBot() == null);
}
public void sendMessageIfFromCommittedBot(MessageDTO messageDTO) {
ChatDTO chat = chatService.getOrCreateChatByMessage(messageDTO);
ChatDTO chat = chatService.getChatByMessage(messageDTO);
if (chat != null && messageDTO.getSender().equals(chat.getCommittedBot())) {
saveAndSendMessage(messageDTO);
messageService.saveAndSendMessage(messageDTO);
} else {
log.debug("Bot {} not assigned to Chat: {} - ignoring message: {}", messageDTO.getSender(), chat, messageDTO);
}
}
public ChatMessageCollector addMessageToChatMessageCollection(MessageDTO messageDTO) {
ChatDTO chatDTO = chatService.getOrCreateChatByMessage(messageDTO);
ChatDTO chatDTO = chatService.getChatByMessage(messageDTO);
ChatMessageCollector chatMessageCollector = getOrCreateChatMessageCollector(chatDTO);
chatMessageCollector.messages.add(messageDTO);
......@@ -102,11 +97,11 @@ public class ConversationService {
chatMessageCollectors.remove(chatMessageCollector);
if (isRemarkableIntent(highestConfidenceMessage.getInterpretedIntent())) {
commitBotToChat(highestConfidenceMessage, chatMessageCollector.chat);
saveAndSendMessage(highestConfidenceMessage);
chatService.commitBotToChat(highestConfidenceMessage, chatMessageCollector.chat);
messageService.saveAndSendMessage(highestConfidenceMessage);
return;
}
saveAndSendMessage(addressedBotMessage);
messageService.saveAndSendMessage(addressedBotMessage);
} catch (NoSuchElementException | ArithmeticException e) {
log.error("Encountered exception when committing bot to chat", e);
}
......@@ -115,56 +110,4 @@ public class ConversationService {
private boolean isRemarkableIntent(String intent) {
return !EnumUtils.isValidEnum(UnremarkableIntent.class, intent);
}
public void commitBotToChat(MessageDTO messageDTO, ChatDTO chatDTO) {
log.debug("Committing responder {} to chat {}", messageDTO.getSender(), chatDTO.getId());
chatDTO.setCommittedBot(messageDTO.getSender());
chatService.save(chatDTO);
}
public void commitAdminToChat(ChatDTO chatDTO, Principal principal) {
log.debug("Committing admin {} to chat {}", principal.getName(), chatDTO.getId());
chatDTO.setCommittedAdmin(principal.getName());
chatService.save(chatDTO);
}
public void saveAndSendMessage(MessageDTO messageDTO) {
MessageDTO savedMessage = updateChatAndSaveMessage(messageDTO);
sendMessageToUser(savedMessage, savedMessage.getSessionId());
sendMessageToAdmin(savedMessage);
}
public MessageDTO updateChatAndSaveMessage(MessageDTO messageDTO) {
ChatDTO messageChat = chatService.getOrCreateChatByMessage(messageDTO);
MessageDTO savedMessage = messageService.save(messageDTO);
messageChat.setLastRespondedBot(savedMessage.getSender());
if (messageDTO.getText().equals("/restart")) {
messageChat.setCommittedBot(null);
}
if (messageDTO.getSender() == null) {
messageChat.setLastMessageId(savedMessage.getId());
}
chatService.save(messageChat);
return savedMessage;
}
public MessageDTO sendMessageToUser(@Payload MessageDTO messageDTO, String sessionId) {
log.debug("Sending message: {} to session: {}", messageDTO, sessionId);
messagingTemplate.convertAndSend("/topic/chat/" + sessionId, messageDTO);
return messageDTO;
}
public MessageDTO sendMessageToAdmin(@Payload MessageDTO messageDTO) {
log.debug("Sending message: {} to admin", messageDTO);
chatService.findOne(messageDTO.getChatId()).ifPresent(chatDTO ->
messagingTemplate.convertAndSend("/topic/admin/" + chatDTO.getCommittedBot(), messageDTO
));
return messageDTO;
}
public boolean chatHasCommittedAdmin(Long chatId) {
return chatService.findOne(chatId).orElse(new ChatDTO()).getCommittedAdmin() != null;
}
}
......@@ -15,6 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
......@@ -33,10 +34,14 @@ public class MessageService {
private final SimpMessageSendingOperations messagingTemplate;
public MessageService(MessageRepository messageRepository, MessageMapper messageMapper, SimpMessageSendingOperations messagingTemplate) {
private final ChatService chatService;
public MessageService(MessageRepository messageRepository, MessageMapper messageMapper,
SimpMessageSendingOperations messagingTemplate, ChatService chatService) {
this.messageRepository = messageRepository;
this.messageMapper = messageMapper;
this.messagingTemplate = messagingTemplate;
this.chatService = chatService;
}
/**
......@@ -112,6 +117,26 @@ public class MessageService {
}
}
public void saveAndSendMessage(MessageDTO messageDTO) {
MessageDTO savedMessage = save(messageDTO);
sendMessageToUser(savedMessage, savedMessage.getSessionId());
sendMessageToAdmin(savedMessage);
}
public MessageDTO sendMessageToUser(@Payload MessageDTO messageDTO, String sessionId) {
log.debug("Sending message: {} to session: {}", messageDTO, sessionId);
messagingTemplate.convertAndSend("/topic/chat/" + sessionId, messageDTO);
return messageDTO;
}
public MessageDTO sendMessageToAdmin(@Payload MessageDTO messageDTO) {
log.debug("Sending message: {} to admin", messageDTO);
chatService.findOne(messageDTO.getChatId()).ifPresent(chatDTO ->
messagingTemplate.convertAndSend("/topic/admin/" + chatDTO.getCommittedBot(), messageDTO
));
return messageDTO;
}
public void broadcastMessageToUserGroup(String usergroup, MessageDTO messageDTO) {
log.debug("Broadcasting message: {} to userGroup {}", messageDTO, usergroup);
messagingTemplate.convertAndSend("/topic/admin/" + usergroup, messageDTO);
......
......@@ -46,6 +46,9 @@ public class ChatDTO implements Serializable {
@Size(max = 255)
private String lastRespondedBot;
@Size(max = 1000)
private String lastUserReferer;
private Boolean hasBeenLabeled;
public Long getId() {
......@@ -168,6 +171,14 @@ public class ChatDTO implements Serializable {
this.hasBeenLabeled = hasBeenLabeled;
}
public String getLastUserReferer() {
return lastUserReferer;
}
public void setLastUserReferer(String lastUserReferer) {
this.lastUserReferer = lastUserReferer;
}
@Override
public boolean equals(Object o) {
if (this == o) {
......
package com.netgroup.riigibot.web;
public class ConversationEventConstants {
public static final String USER_LEFT_CONVERSATION = "USER_HAS_LEFT";
public static final String CHAT_REQUIRES_CUSTOMER_SERVICE = "CHAT_REQUIRES_CUSTOMER_SERVICE";
private ConversationEventConstants() {}
}
......@@ -27,6 +27,8 @@ import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import static com.netgroup.riigibot.web.ConversationEventConstants.CHAT_REQUIRES_CUSTOMER_SERVICE;
/**
* REST controller for managing {@link com.netgroup.riigibot.domain.Chat}.
*/
......@@ -37,8 +39,6 @@ public class ChatResource {
private static final String ENTITY_NAME = "chat";
private static final String CHAT_REQUIRES_CUSTOMER_SERVICE = "CHAT_REQUIRES_CUSTOMER_SERVICE";
@Value("${jhipster.clientApp.name}")
private String applicationName;
......
......@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.netgroup.riigibot.config.KafkaProperties;
import com.netgroup.riigibot.service.ChatService;
import com.netgroup.riigibot.service.ConversationService;
import com.netgroup.riigibot.service.MessageService;
import com.netgroup.riigibot.service.dto.ChatDTO;
import com.netgroup.riigibot.service.dto.MessageDTO;
import com.netgroup.riigibot.web.rest.errors.BadRequestAlertException;
......@@ -24,10 +25,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
/**
* REST controller for managing conversations between clients and bots
......@@ -43,15 +41,17 @@ public class ConversationResource {
private String applicationName;
private final ConversationService conversationService;
private final MessageService messageService;
private final ChatService chatService;
private final ExecutorService sseExecutorService = Executors.newCachedThreadPool();
private final KafkaProperties kafkaProperties;
private final KafkaProducer<String, MessageDTO> producer;
public ConversationResource(KafkaProperties kafkaProperties, ConversationService conversationService, ChatService chatService) {
public ConversationResource(KafkaProperties kafkaProperties, ConversationService conversationService, MessageService messageService, ChatService chatService) {
this.kafkaProperties = kafkaProperties;
this.producer = new KafkaProducer<>(kafkaProperties.getProducerProps());
this.conversationService = conversationService;
this.messageService = messageService;
this.chatService = chatService;
executeKafkaConsumer();
}
......@@ -93,17 +93,26 @@ public class ConversationResource {
* @return the {@link ResponseEntity} with status {@code 200 (OK)}
*/
@PostMapping("/conversation")
public ResponseEntity<String> createAndSendUserMessage(@Valid @RequestBody MessageDTO userMessage)
public ResponseEntity<String> sendMessage(@Valid @RequestBody MessageDTO userMessage, @RequestHeader("Referer") String referer)
throws ExecutionException, InterruptedException {
log.debug("REST request to create and send user message : {}", userMessage);
if (userMessage.getId() != null) {
throw new BadRequestAlertException("A new message cannot already have an ID", ENTITY_NAME, "idexists");
}
MessageDTO savedUserMessage = conversationService.updateChatAndSaveMessage(userMessage);
conversationService.sendMessageToAdmin(savedUserMessage);
ChatDTO chatDTO;
if (userMessage.getChatId() == null) {
chatDTO = chatService.createNewChatByMessage(userMessage);
userMessage.setChatId(chatDTO.getId());
} else {
chatDTO = chatService.getChatByMessage(userMessage);
}
chatDTO.setLastUserReferer(referer);
MessageDTO savedUserMessage = messageService.save(userMessage);
chatService.updateChatWithMessageInformation(chatDTO, savedUserMessage);
if (!conversationService.chatHasCommittedAdmin(savedUserMessage.getChatId())) {
messageService.sendMessageToAdmin(savedUserMessage);
if (!chatService.isChatHandledByCustomerSupport(chatDTO)) {
log.debug(
"Request to send to Kafka topic 'user_events' with key {} the message : {}",
savedUserMessage.getChatId().toString(),
......@@ -121,17 +130,15 @@ public class ConversationResource {
@PostMapping("/conversation/user")
public ResponseEntity<Void> sendDirectMessageToUser(@Valid @RequestBody MessageDTO messageDTO) {
MessageDTO savedMessage = conversationService.updateChatAndSaveMessage(messageDTO);
conversationService.sendMessageToUser(savedMessage, savedMessage.getSessionId());
conversationService.sendMessageToAdmin(savedMessage);
messageService.saveAndSendMessage(messageDTO);
return ResponseEntity.ok().build();
}
@PostMapping("/conversation/user/commit")
public ResponseEntity<ChatDTO> assignChatResponder(@Valid @RequestBody MessageDTO messageDTO, Principal principal) {
ChatDTO chatDTO = chatService.getOrCreateChatByMessage(messageDTO);
conversationService.commitAdminToChat(chatDTO, principal);
conversationService.saveAndSendMessage(messageDTO);
ChatDTO chatDTO = chatService.getChatByMessage(messageDTO);
chatService.commitAdminToChat(chatDTO, principal);
messageService.saveAndSendMessage(messageDTO);
return ResponseEntity.ok(chatDTO);
}
}
......@@ -2,7 +2,7 @@ package com.netgroup.riigibot.web.websocket;
import com.netgroup.riigibot.domain.Chat;
import com.netgroup.riigibot.repository.ChatRepository;
import com.netgroup.riigibot.service.ConversationService;
import com.netgroup.riigibot.service.MessageService;
import com.netgroup.riigibot.service.dto.MessageDTO;
import java.time.Instant;
import org.slf4j.Logger;
......@@ -11,15 +11,17 @@ import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Controller;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import static com.netgroup.riigibot.web.ConversationEventConstants.USER_LEFT_CONVERSATION;
@Controller
public class DisconnectHandler implements ApplicationListener<SessionDisconnectEvent> {
private static final Logger log = LoggerFactory.getLogger(DisconnectHandler.class);
private final ChatRepository chatRepository;
private final ConversationService conversationService;
private final MessageService messageService;
public DisconnectHandler(ChatRepository chatRepository, ConversationService conversationService) {
public DisconnectHandler(ChatRepository chatRepository, MessageService messageService) {
this.chatRepository = chatRepository;
this.conversationService = conversationService;
this.messageService = messageService;
}
@Override
......@@ -32,9 +34,9 @@ public class DisconnectHandler implements ApplicationListener<SessionDisconnectE
MessageDTO quitMessage = new MessageDTO();
quitMessage.setChatId(chat.getId());
quitMessage.setText("USER_HAS_LEFT");
quitMessage.setText(USER_LEFT_CONVERSATION);
quitMessage.setCreated(Instant.now());
conversationService.sendMessageToAdmin(quitMessage);
messageService.sendMessageToAdmin(quitMessage);
}
}
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.9.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<!--
Added new field to chat
-->
<changeSet id="20201021113000-1" author="henrik_prangel">
<addColumn tableName="chat">
<column name="last_user_referer" type="varchar(1000)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
......@@ -25,6 +25,7 @@
<include file="config/liquibase/changelog/20201014152000_created_new_user_roles.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20201015101000_added_labeled_row_to_chat.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20201019140000_added_vote_field_to_message_entity.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20201021113000_added_last_referer_field_to_chat.xml" relativeToChangelogFile="false"/>
<!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here -->
<!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here -->
</databaseChangeLog>
import SockJS from 'sockjs-client';
import axios from 'axios';
import Stomp from 'webstomp-client';
import { Observable } from 'rxjs';
......@@ -129,23 +130,10 @@ export default store => next => action => {
const accountId = store.getState().authentication.account.id;
const chatExists = store.getState().customerService.chats.chatList.find(chat => chat.id === activity.chatId);
if (activity.chatId && !chatExists) {
return store.dispatch({
type: CHAT_ACTIONS.ADD_NEW_CHAT,
payload: {
id: activity.chatId,
topic: null,
created: null,
updated: activity.created,
deleted: null,
ended: activity.ended,
lastMessageId: activity.id,
lastMessageText: activity.text,
sessionId: activity.sessionId,
committedAdmin: activity.authorLoginName,
committedBot: activity.sender,
},
payload: axios.get(`api/chats/${activity.chatId}`),
});
}
......
......@@ -73,7 +73,10 @@ export const AdditionalContent = (props: IAdditionalContentProp) => {
contentKey="customerService.additionalContent.conversationDetails">Conversation Details</Translate>
</span>
<hr className="divider"/>
<div>Asub lehel: naidis.com/vestlus (PH)</div>
<div>
<Translate contentKey="customerService.additionalContent.userLocation">User location</Translate>
{activeChat.lastUserReferer}
</div>
<div>
<span>
<Translate contentKey="customerService.additionalContent.assignedAdmin">Assigned admin</Translate>
......
......@@ -59,16 +59,28 @@ export default (state = initialState, action) => {
chatList: action.payload.data,
};
}
case ACTION_TYPES.SET_ACTIVE_CHAT: {
case SUCCESS(ACTION_TYPES.ADD_NEW_CHAT): {
return produce(state, draftState => {
draftState.activeChat = state.chatList.find(c => c.id === action.payload);
draftState.chatList.push(action.payload.data);
});
}
case ACTION_TYPES.ADD_NEW_CHAT: {
case SUCCESS(ACTION_TYPES.START_CONVERSATION): {
return produce(state, draftState => {
draftState.chatList.push(action.payload);
const {id, committedAdmin} = action.payload.data;
const {chatList, activeChat} = draftState;
const index = chatList.findIndex(chat => id === chat.id);
chatList[index].committedAdmin = committedAdmin;
if (activeChat.id === id) {
activeChat.committedAdmin = committedAdmin;
}
});
}
case ACTION_TYPES.SET_ACTIVE_CHAT: {
return produce(state, draftState => {
draftState.activeChat = state.chatList.find(c => c.id === action.payload);
});
}
case ACTION_TYPES.UPDATE_LAST_MESSAGE: {
return produce(state, draftState => {
const {id, text, created, authorLoginName, chatId} = action.payload;
......@@ -96,17 +108,6 @@ export default (state = initialState, action) => {
}
});