import { ServiceClassInternal, Message } from '@rocket.chat/core-services'; import type { IOmnichannelEEService } from '@rocket.chat/core-services'; import { isOmnichannelRoom, LivechatInquiryStatus } from '@rocket.chat/core-typings'; import type { IOmnichannelRoom, IUser, ILivechatInquiryRecord, IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatRooms, Subscriptions, LivechatInquiry } from '@rocket.chat/models'; import { callbacks } from '../../../server/lib/callbacks'; import { notifyOnSubscriptionChangedByRoomId, notifyOnLivechatInquiryChangedById, notifyOnRoomChangedById, } from '../../../server/lib/notifyListener'; import { dispatchAgentDelegated } from '../../../server/lib/omnichannel/Helper'; import { queueInquiry } from '../../../server/lib/omnichannel/QueueManager'; import { RoutingManager } from '../../../server/lib/omnichannel/RoutingManager'; import { settings } from '../../../server/settings'; export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelEEService { protected name = 'omnichannel-ee'; protected override internal = true; logger: Logger; constructor() { super(); this.logger = new Logger('OmnichannelEE'); } async placeRoomOnHold( room: Pick, comment: string, onHoldBy: Pick, ) { this.logger.debug({ msg: 'Attempting to place room on hold', roomId: room._id, userId: onHoldBy?._id }); const { _id: roomId } = room; if (!room || !isOmnichannelRoom(room)) { throw new Error('error-invalid-room'); } if (!room.open) { throw new Error('error-room-already-closed'); } if (room.onHold) { throw new Error('error-room-is-already-on-hold'); } const restrictedOnHold = settings.get('Livechat_allow_manual_on_hold_upon_agent_engagement_only'); const canRoomBePlacedOnHold = !room.onHold; const canAgentPlaceOnHold = !room.lastMessage?.token; const canPlaceChatOnHold = canRoomBePlacedOnHold && (!restrictedOnHold || canAgentPlaceOnHold); if (!canPlaceChatOnHold) { throw new Error('error-cannot-place-chat-on-hold'); } if (!room.servedBy) { throw new Error('error-unserved-rooms-cannot-be-placed-onhold'); } const [roomResult, subsResult] = await Promise.all([ LivechatRooms.setOnHoldByRoomId(roomId), Subscriptions.setOnHoldByRoomId(roomId), Message.saveSystemMessage('omnichannel_placed_chat_on_hold', roomId, '', onHoldBy, { comment }), ]); if (roomResult.modifiedCount) { void notifyOnRoomChangedById(roomId); } if (subsResult.modifiedCount) { void notifyOnSubscriptionChangedByRoomId(roomId); } await callbacks.run('livechat:afterOnHold', room); } async resumeRoomOnHold( room: Pick, comment: string, resumeBy: Pick, clientAction = false, ) { this.logger.debug({ msg: 'Attempting to resume room on hold', roomId: room._id, userId: resumeBy?._id }); if (!room || !isOmnichannelRoom(room)) { throw new Error('error-invalid-room'); } if (!room.open) { throw new Error('This_conversation_is_already_closed'); } if (!room.onHold) { throw new Error('error-room-not-on-hold'); } const { _id: roomId, servedBy } = room; if (!servedBy) { this.logger.error({ msg: 'No serving agent found for room', roomId }); throw new Error('error-room-not-served'); } const inquiry = await LivechatInquiry.findOneByRoomId(roomId, {}); if (!inquiry) { this.logger.error({ msg: 'No inquiry found for room', roomId }); throw new Error('error-invalid-inquiry'); } await this.attemptToAssignRoomToServingAgentElseQueueIt({ room, inquiry, servingAgent: servedBy, clientAction, }); const [roomResult, subsResult] = await Promise.all([ LivechatRooms.unsetOnHoldByRoomId(roomId), Subscriptions.unsetOnHoldByRoomId(roomId), Message.saveSystemMessage('omnichannel_on_hold_chat_resumed', roomId, '', resumeBy, { comment }), ]); if (roomResult.modifiedCount) { void notifyOnRoomChangedById(roomId); } if (subsResult.modifiedCount) { void notifyOnSubscriptionChangedByRoomId(roomId); } await callbacks.run('livechat:afterOnHoldChatResumed', room); } private async attemptToAssignRoomToServingAgentElseQueueIt({ room, inquiry, servingAgent, clientAction, }: { room: Pick; inquiry: ILivechatInquiryRecord; servingAgent: NonNullable; clientAction: boolean; }) { try { const agent = { agentId: servingAgent._id, username: servingAgent.username, }; await callbacks.run('livechat.checkAgentBeforeTakeInquiry', { agent, inquiry, options: {}, }); return; } catch (e) { this.logger.error({ msg: 'Agent is not available to take inquiry', agentId: servingAgent._id, inquiryId: inquiry._id, err: e }); if (clientAction) { // if the action was triggered by the client, we should throw the error // so the client can handle it and show the error message to the user throw e; } } await this.removeCurrentAgentFromRoom({ room, inquiry }); const { _id: inquiryId } = inquiry; const newInquiry = await LivechatInquiry.findOneById(inquiryId); if (!newInquiry) { throw new Error('error-invalid-inquiry'); } await queueInquiry(newInquiry); } private async removeCurrentAgentFromRoom({ room, inquiry, }: { room: Pick; inquiry: ILivechatInquiryRecord; }): Promise { this.logger.debug({ msg: 'Attempting to remove current agent from room', roomId: room._id }); const { _id: roomId } = room; const { _id: inquiryId } = inquiry; await Promise.all([ LivechatRooms.removeAgentByRoomId(roomId), LivechatInquiry.queueInquiryAndRemoveDefaultAgent(inquiryId), RoutingManager.removeAllRoomSubscriptions(room), ]); void notifyOnLivechatInquiryChangedById(inquiryId, 'updated', { status: LivechatInquiryStatus.QUEUED, queuedAt: new Date(), takenAt: undefined, defaultAgent: undefined, }); await dispatchAgentDelegated(roomId); void notifyOnRoomChangedById(roomId); } }