From fe32b1362b82d27152e7f10cfc2a4339afd48a50 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 5 Jan 2024 10:07:04 -0600 Subject: [PATCH 1/8] initial --- .../server/hooks/beforeDelegateAgent.ts | 25 ------ apps/meteor/app/livechat/server/index.ts | 1 - apps/meteor/app/livechat/server/lib/Helper.ts | 11 +-- .../app/livechat/server/lib/LivechatTyped.ts | 86 ++++++++++++------- .../app/livechat/server/lib/QueueManager.ts | 28 +++--- .../app/livechat/server/lib/RoutingManager.ts | 71 ++++++++++----- .../server/hooks/beforeRoutingChat.ts | 27 +++--- .../hooks/handleNextAgentPreferredEvents.ts | 19 ++-- apps/meteor/lib/callbacks.ts | 1 - apps/meteor/server/services/voip/service.ts | 12 +-- 10 files changed, 150 insertions(+), 131 deletions(-) delete mode 100644 apps/meteor/app/livechat/server/hooks/beforeDelegateAgent.ts diff --git a/apps/meteor/app/livechat/server/hooks/beforeDelegateAgent.ts b/apps/meteor/app/livechat/server/hooks/beforeDelegateAgent.ts deleted file mode 100644 index 87a24b0d7a34..000000000000 --- a/apps/meteor/app/livechat/server/hooks/beforeDelegateAgent.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { LivechatDepartmentAgents, Users } from '@rocket.chat/models'; - -import { callbacks } from '../../../../lib/callbacks'; -import { settings } from '../../../settings/server'; - -callbacks.add( - 'livechat.beforeDelegateAgent', - async (agent, { department } = {}) => { - if (agent) { - return agent; - } - - if (!settings.get('Livechat_assign_new_conversation_to_bot')) { - return null; - } - - if (department) { - return LivechatDepartmentAgents.getNextBotForDepartment(department); - } - - return Users.getNextBotAgent(); - }, - callbacks.priority.HIGH, - 'livechat-before-delegate-agent', -); diff --git a/apps/meteor/app/livechat/server/index.ts b/apps/meteor/app/livechat/server/index.ts index fc96f2a921a9..7045275e0412 100644 --- a/apps/meteor/app/livechat/server/index.ts +++ b/apps/meteor/app/livechat/server/index.ts @@ -1,7 +1,6 @@ import './livechat'; import './startup'; import '../lib/messageTypes'; -import './hooks/beforeDelegateAgent'; import './hooks/leadCapture'; import './hooks/markRoomResponded'; import './hooks/offlineMessage'; diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index c1acc87018e8..dbf21fa938cb 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -117,11 +117,10 @@ export const createLivechatRoom = async ( ); const roomId = (await Rooms.insertOne(room)).insertedId; + await sendMessage(guest, { t: 'livechat-started', msg: '', groupable: false }, room); void Apps.triggerEvent(AppEvents.IPostLivechatRoomStarted, room); - await callbacks.run('livechat.newRoom', room); - - await sendMessage(guest, { t: 'livechat-started', msg: '', groupable: false }, room); + void callbacks.run('livechat.newRoom', room); return roomId; }; @@ -193,7 +192,6 @@ export const createLivechatInquiry = async ({ }; const result = (await LivechatInquiry.insertOne(inquiry)).insertedId; - logger.debug(`Inquiry ${result} created for visitor ${_id}`); return result; }; @@ -338,7 +336,6 @@ export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, age if (!inquiry?._id) { return; } - logger.debug(`Notifying agents of new inquiry ${inquiry._id} queued`); const { department, rid, v } = inquiry; const room = await LivechatRooms.findOneById(rid); @@ -358,12 +355,12 @@ export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, age // Alert only the online agents of the queued request const onlineAgents = await LivechatTyped.getOnlineAgents(department, agent); - if (!onlineAgents) { + const total = await onlineAgents?.count(); + if (!onlineAgents || !total) { logger.debug('Cannot notify agents of queued inquiry. No online agents found'); return; } - logger.debug(`Notifying ${await onlineAgents.count()} agents of new inquiry`); const notificationUserName = v && (v.name || v.username); for await (const agent of onlineAgents) { diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index ea508b047882..2939a81a08e8 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -360,6 +360,44 @@ class LivechatClass { } } + async getNewRoom( + guest: ILivechatVisitor, + message: Pick, + roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + }, + agent?: SelectedAgent, + extraData?: Record, + ) { + const defaultAgent = await callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, guest); + // if no department selected verify if there is at least one active and pick the first + if (!defaultAgent && !guest.department) { + const department = await this.getRequiredDepartment(); + if (department) { + this.logger.debug({ + msg: 'No department or default agent selected. Assigning to department', + visitorId: guest._id, + department, + }); + guest.department = department._id; + } + } + + // delegate room creation to QueueManager + this.logger.debug({ + msg: 'Calling QueueManager to request a room', + visitorId: guest._id, + }); + return QueueManager.requestRoom({ + guest, + message, + roomInfo, + agent: defaultAgent, + extraData, + }); + } + async getRoom( guest: ILivechatVisitor, message: Pick, @@ -373,14 +411,23 @@ class LivechatClass { if (!this.enabled()) { throw new Meteor.Error('error-omnichannel-is-disabled'); } - Livechat.logger.debug(`Attempting to find or create a room for visitor ${guest._id}`); + + this.logger.debug({ + msg: 'Getting room for visitor', + visitor: guest, + }); + let room = await LivechatRooms.findOneById(message.rid); let newRoom = false; if (room && !room.open) { - Livechat.logger.debug(`Last room for visitor ${guest._id} closed. Creating new one`); message.rid = Random.id(); room = null; + + this.logger.debug({ + msg: 'Room is closed. Opening new one', + room: message.rid, + }); } if ( @@ -395,41 +442,20 @@ class LivechatClass { } if (room == null) { - const defaultAgent = await callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, guest); - // if no department selected verify if there is at least one active and pick the first - if (!defaultAgent && !guest.department) { - const department = await this.getRequiredDepartment(); - Livechat.logger.debug(`No department or default agent selected for ${guest._id}`); - - if (department) { - Livechat.logger.debug(`Assigning ${guest._id} to department ${department._id}`); - guest.department = department._id; - } - } - - // delegate room creation to QueueManager - Livechat.logger.debug(`Calling QueueManager to request a room for visitor ${guest._id}`); - room = await QueueManager.requestRoom({ - guest, - message, - roomInfo, - agent: defaultAgent, - extraData, - }); + room = await this.getNewRoom(guest, message, roomInfo, agent, extraData); + await Messages.setRoomIdByToken(guest.token, room._id); newRoom = true; - - Livechat.logger.debug(`Room obtained for visitor ${guest._id} -> ${room._id}`); } if (!room || room.v.token !== guest.token) { - Livechat.logger.debug(`Visitor ${guest._id} trying to access another visitor's room`); + this.logger.error({ + msg: 'Room does not exist or belongs to another visitor', + visitorId: guest._id, + roomId: room?._id, + }); throw new Meteor.Error('cannot-access-room'); } - if (newRoom) { - await Messages.setRoomIdByToken(guest.token, room._id); - } - return { room, newRoom }; } diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 81648c7449f0..fe66efbc6f17 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -18,9 +18,13 @@ export const saveQueueInquiry = async (inquiry: ILivechatInquiryRecord) => { export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent?: SelectedAgent) => { const inquiryAgent = await RoutingManager.delegateAgent(defaultAgent, inquiry); - logger.debug(`Delegating inquiry with id ${inquiry._id} to agent ${defaultAgent?.username}`); + logger.debug({ + msg: 'Routing inquiry', + inquiryId: inquiry._id, + inquiryAgent, + }); - await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent); + const dbInquiry = await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent); const room = await LivechatRooms.findOneById(inquiry.rid, { projection: { v: 1 } }); if (!room || !(await Omnichannel.isWithinMACLimit(room))) { logger.error({ msg: 'MAC limit reached, not routing inquiry', inquiry }); @@ -29,11 +33,6 @@ export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent await saveQueueInquiry(inquiry); return; } - const dbInquiry = await LivechatInquiry.findOneById(inquiry._id); - - if (!dbInquiry) { - throw new Error('inquiry-not-found'); - } if (dbInquiry.status === 'ready') { logger.debug(`Inquiry with id ${inquiry._id} is ready. Delegating to agent ${inquiryAgent?.username}`); @@ -85,10 +84,8 @@ export const QueueManager: queueManager = { const room = await LivechatRooms.findOneById(await createLivechatRoom(rid, name, guest, roomInfo, extraData)); if (!room) { - logger.error(`Room for visitor ${guest._id} not found`); throw new Error('room-not-found'); } - logger.debug(`Room for visitor ${guest._id} created with id ${room._id}`); const inquiry = await LivechatInquiry.findOneById( await createLivechatInquiry({ @@ -100,18 +97,25 @@ export const QueueManager: queueManager = { }), ); if (!inquiry) { - logger.error(`Inquiry for visitor ${guest._id} not found`); throw new Error('inquiry-not-found'); } + logger.debug({ + msg: 'New room created', + rid, + inquiryId: inquiry._id, + }); + await LivechatRooms.updateRoomCount(); await queueInquiry(inquiry, agent); - logger.debug(`Inquiry ${inquiry._id} queued`); + logger.debug({ + msg: 'Inquiry queued', + inquiryId: inquiry._id, + }); const newRoom = await LivechatRooms.findOneById(rid); if (!newRoom) { - logger.error(`Room with id ${rid} not found`); throw new Error('room-not-found'); } diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index 7b85c31f26ac..cadbadf500b3 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -12,12 +12,13 @@ import type { } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; -import { LivechatInquiry, LivechatRooms, Subscriptions, Rooms, Users } from '@rocket.chat/models'; +import { LivechatInquiry, LivechatRooms, Subscriptions, Rooms, Users, LivechatDepartmentAgents } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; +import { settings } from '../../../settings/server'; import { createLivechatSubscription, dispatchAgentDelegated, @@ -46,7 +47,7 @@ type Routing = { agent?: SelectedAgent | null, options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any } }, ): Promise<(IOmnichannelRoom & { chatQueued?: boolean }) | null | void>; - assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent): Promise; + assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent, room: IOmnichannelRoom | null): Promise; unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string): Promise; takeInquiry( inquiry: Omit< @@ -59,6 +60,7 @@ type Routing = { transferRoom(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData): Promise; delegateAgent(agent: SelectedAgent | undefined, inquiry: ILivechatInquiryRecord): Promise; removeAllRoomSubscriptions(room: Pick, ignoreUser?: { _id: string }): Promise; + getDefaultAgent(agent: SelectedAgent | undefined, { department }: { department?: string }): Promise; }; export const RoutingManager: Routing = { @@ -120,10 +122,20 @@ export const RoutingManager: Routing = { async delegateInquiry(inquiry, agent, options = {}) { const { department, rid } = inquiry; logger.debug(`Attempting to delegate inquiry ${inquiry._id}`); - if (!agent || (agent.username && !(await Users.findOneOnlineAgentByUserList(agent.username)) && !(await allowAgentSkipQueue(agent)))) { - logger.debug(`Agent offline or invalid. Using routing method to get next agent for inquiry ${inquiry._id}`); + if ( + !agent || + (agent.username && + !(await Users.findOneOnlineAgentByUserList(agent.username, { projection: { _id: 1 } })) && + !(await allowAgentSkipQueue(agent))) + ) { agent = await this.getNextAgent(department); - logger.debug(`Routing method returned agent ${agent?.agentId} for inquiry ${inquiry._id}`); + logger.debug({ + msg: 'Selected agent was offline or invalid. Got new agent with routhing method', + agent, + department, + inquiryId: inquiry._id, + routing: this.methodName, + }); } if (!agent) { @@ -137,7 +149,7 @@ export const RoutingManager: Routing = { return this.takeInquiry(inquiry, agent, options); }, - async assignAgent(inquiry, agent) { + async assignAgent(inquiry, agent, room) { check( agent, Match.ObjectIncluding({ @@ -150,29 +162,23 @@ export const RoutingManager: Routing = { const { rid, name, v, department } = inquiry; if (!(await createLivechatSubscription(rid, name, v, agent, department))) { - logger.debug(`Cannot assign agent to inquiry ${inquiry._id}: Cannot create subscription`); throw new Meteor.Error('error-creating-subscription', 'Error creating subscription'); } - await LivechatRooms.changeAgentByRoomId(rid, agent); - await Rooms.incUsersCountById(rid, 1); - + await Promise.all([LivechatRooms.changeAgentByRoomId(rid, agent), Rooms.incUsersCountById(rid, 1)]); const user = await Users.findOneById(agent.agentId); - const room = await LivechatRooms.findOneById(rid); if (user) { await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]); } if (!room) { - logger.debug(`Cannot assign agent to inquiry ${inquiry._id}: Room not found`); throw new Meteor.Error('error-room-not-found', 'Room not found'); } - await dispatchAgentDelegated(rid, agent.agentId); - logger.debug(`Agent ${agent.agentId} assigned to inquriy ${inquiry._id}. Instances notified`); - + void dispatchAgentDelegated(rid, agent.agentId); void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); + return inquiry; }, @@ -256,15 +262,14 @@ export const RoutingManager: Routing = { if (!agent) { logger.debug(`Cannot take Inquiry ${inquiry._id}: Precondition failed for agent`); - const cbRoom = await callbacks.run<'livechat.onAgentAssignmentFailed'>('livechat.onAgentAssignmentFailed', room, { + return callbacks.run<'livechat.onAgentAssignmentFailed'>('livechat.onAgentAssignmentFailed', room, { inquiry, options, }); - return cbRoom; } await LivechatInquiry.takeInquiry(_id); - const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, agent); + const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, agent, room); logger.info(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); callbacks.runAsync('livechat.afterTakeInquiry', inq, agent); @@ -288,18 +293,40 @@ export const RoutingManager: Routing = { return false; }, + async getDefaultAgent( + agent: SelectedAgent | undefined, + { department }: { department?: string }, + ): Promise { + if (agent) { + return agent; + } + + if (!settings.get('Livechat_assign_new_conversation_to_bot')) { + return null; + } + + if (department) { + return LivechatDepartmentAgents.getNextBotForDepartment(department); + } + + return Users.getNextBotAgent(); + }, + async delegateAgent(agent, inquiry) { - const defaultAgent = await callbacks.run('livechat.beforeDelegateAgent', agent, { + const defaultAgent = await this.getDefaultAgent(agent, { department: inquiry?.department, }); if (defaultAgent) { - logger.debug(`Delegating Inquiry ${inquiry._id} to agent ${defaultAgent.username}`); + logger.debug({ + msg: `Delegating Inquiry ${inquiry._id} to agent ${defaultAgent.username}`, + inquiryId: inquiry._id, + agentId: defaultAgent.username, + }); await LivechatInquiry.setDefaultAgentById(inquiry._id, defaultAgent); } - logger.debug(`Queueing inquiry ${inquiry._id}`); - await dispatchInquiryQueued(inquiry, defaultAgent); + void dispatchInquiryQueued(inquiry, defaultAgent); return defaultAgent; }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts index 69c3914cb4d8..8bc4eeb97b45 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts @@ -14,6 +14,9 @@ import { cbLogger } from '../lib/logger'; callbacks.add( 'livechat.beforeRouteChat', async (inquiry, agent) => { + if (!inquiry) { + return inquiry; + } // check here if department has fallback before queueing if (inquiry?.department && !(await online(inquiry.department, true, true))) { const department = await LivechatDepartment.findOneById>( @@ -31,14 +34,16 @@ callbacks.add( `Inquiry ${inquiry._id} will be moved from department ${department._id} to fallback department ${department.fallbackForwardDepartment}`, ); // update visitor - await Livechat.setDepartmentForGuest({ - token: inquiry?.v?.token, - department: department.fallbackForwardDepartment, - }); - // update inquiry - inquiry = (await LivechatInquiry.setDepartmentByInquiryId(inquiry._id, department.fallbackForwardDepartment)) ?? inquiry; - // update room - await LivechatRooms.setDepartmentByRoomId(inquiry.rid, department.fallbackForwardDepartment); + + const [inq] = await Promise.all([ + LivechatInquiry.setDepartmentByInquiryId(inquiry._id, department.fallbackForwardDepartment), + Livechat.setDepartmentForGuest({ + token: inquiry?.v?.token, + department: department.fallbackForwardDepartment, + }), + LivechatRooms.setDepartmentByRoomId(inquiry.rid, department.fallbackForwardDepartment), + ]); + inquiry = inq ?? inquiry; } } @@ -46,10 +51,6 @@ callbacks.add( return inquiry; } - if (!inquiry) { - return inquiry; - } - const { _id, status, department } = inquiry; if (status !== 'ready') { @@ -69,7 +70,7 @@ callbacks.add( queueSortBy: getInquirySortMechanismSetting(), }); if (inq) { - await dispatchInquiryPosition(inq); + void dispatchInquiryPosition(inq); } } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts index 21fae96e3555..cf654d1c7e65 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts @@ -5,9 +5,6 @@ import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingMa import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; -let contactManagerPreferred = false; -let lastChattedAgentPreferred = false; - const normalizeDefaultAgent = (agent?: Pick | null): SelectedAgent | null => { if (!agent) { return null; @@ -26,8 +23,7 @@ const getDefaultAgent = async (username?: string): Promise }; settings.watch('Livechat_last_chatted_agent_routing', (value) => { - lastChattedAgentPreferred = value; - if (!lastChattedAgentPreferred) { + if (!value) { callbacks.remove('livechat.onMaxNumberSimultaneousChatsReached', 'livechat-on-max-number-simultaneous-chats-reached'); callbacks.remove('livechat.afterTakeInquiry', 'livechat-save-default-agent-after-take-inquiry'); return; @@ -78,10 +74,6 @@ settings.watch('Livechat_last_chatted_agent_routing', (value) => { ); }); -settings.watch('Omnichannel_contact_manager_routing', (value) => { - contactManagerPreferred = value; -}); - callbacks.add( 'livechat.checkDefaultAgentOnNewRoom', async (defaultAgent, defaultGuest) => { @@ -98,12 +90,13 @@ callbacks.add( } const { lastAgent, token, contactManager } = guest; - const guestManager = contactManager?.username && contactManagerPreferred && getDefaultAgent(contactManager?.username); + const guestManager = + contactManager?.username && settings.get('Omnichannel_contact_manager_routing') && getDefaultAgent(contactManager?.username); if (guestManager) { return guestManager; } - if (!lastChattedAgentPreferred) { + if (!settings.get('Livechat_last_chatted_agent_routing')) { return defaultAgent; } @@ -125,9 +118,7 @@ callbacks.add( if (!usernameByRoom) { return defaultAgent; } - const lastRoomAgent = normalizeDefaultAgent( - await Users.findOneOnlineAgentByUserList(usernameByRoom, { projection: { _id: 1, username: 1 } }), - ); + const lastRoomAgent = getDefaultAgent(usernameByRoom); return lastRoomAgent ?? defaultAgent; }, callbacks.priority.MEDIUM, diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index dcaaf3d15ae3..51d270fefaee 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -154,7 +154,6 @@ type ChainedCallbackSignatures = { agentsId: ILivechatAgent['_id'][]; }; 'livechat.applySimultaneousChatRestrictions': (_: undefined, params: { departmentId?: ILivechatDepartmentRecord['_id'] }) => undefined; - 'livechat.beforeDelegateAgent': (agent: SelectedAgent | undefined, params?: { department?: string }) => SelectedAgent | null | undefined; 'livechat.applyDepartmentRestrictions': ( query: FilterOperators, params: { userId: IUser['_id'] }, diff --git a/apps/meteor/server/services/voip/service.ts b/apps/meteor/server/services/voip/service.ts index 93d8bcefd95d..d58273156a9e 100644 --- a/apps/meteor/server/services/voip/service.ts +++ b/apps/meteor/server/services/voip/service.ts @@ -36,7 +36,7 @@ export class VoipService extends ServiceClassInternal implements IVoipService { this.logger = new Logger('VoIPService'); this.commandHandler = new CommandHandler(db); if (!voipEnabled()) { - this.logger.warn({ msg: 'Voip is not enabled. Cant start the service' }); + this.logger.debug({ msg: 'Voip is not enabled. Cant start the service' }); return; } // Init from constructor if we already have @@ -45,9 +45,9 @@ export class VoipService extends ServiceClassInternal implements IVoipService { } async init(): Promise { - this.logger.info('Starting VoIP service'); + this.logger.debug('Starting VoIP service'); if (this.active) { - this.logger.warn({ msg: 'VoIP service already started' }); + this.logger.debug({ msg: 'VoIP service already started' }); return; } @@ -62,9 +62,9 @@ export class VoipService extends ServiceClassInternal implements IVoipService { } async stop(): Promise { - this.logger.info('Stopping VoIP service'); + this.logger.debug('Stopping VoIP service'); if (!this.active) { - this.logger.warn({ msg: 'VoIP service already stopped' }); + this.logger.debug({ msg: 'VoIP service already stopped' }); return; } @@ -79,7 +79,7 @@ export class VoipService extends ServiceClassInternal implements IVoipService { } async refresh(): Promise { - this.logger.info('Restarting VoIP service due to settings changes'); + this.logger.debug('Restarting VoIP service due to settings changes'); try { // Disable voip service await this.stop(); From f32d86f80c8d5e325a7a89d2bffc422e087d765a Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 8 Jan 2024 10:21:20 -0600 Subject: [PATCH 2/8] optimize getrequiredepartment --- .../app/livechat/server/lib/LivechatTyped.ts | 19 +++++++++---------- .../server/models/raw/LivechatDepartment.ts | 15 +++++++++++++++ .../models/raw/LivechatDepartmentAgents.ts | 13 +++++++++++++ apps/meteor/server/models/raw/Users.js | 6 ++++++ .../models/ILivechatDepartmentAgentsModel.ts | 1 + .../src/models/ILivechatDepartmentModel.ts | 5 +++++ .../model-typings/src/models/IUsersModel.ts | 1 + 7 files changed, 50 insertions(+), 10 deletions(-) diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 2939a81a08e8..a823ebe29ed5 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -343,18 +343,17 @@ class LivechatClass { } async getRequiredDepartment(onlineRequired = true) { - const departments = LivechatDepartment.findEnabledWithAgents(); + if (!onlineRequired) { + return LivechatDepartment.findOneEnabledWithAgentsAndAvailableOnRegistration(); + } + const departments = LivechatDepartment.findEnabledWithAgentsAndAvailableOnRegistration(); for await (const dept of departments) { - if (!dept.showOnRegistration) { - continue; - } - if (!onlineRequired) { - return dept; - } - - const onlineAgents = await LivechatDepartmentAgents.getOnlineForDepartment(dept._id); - if (onlineAgents && (await onlineAgents.count())) { + const onlineAgents = await LivechatDepartmentAgents.countOnlineForDepartment( + dept._id, + settings.get('Livechat_enabled_when_agent_idle'), + ); + if (onlineAgents > 0) { return dept; } } diff --git a/apps/meteor/server/models/raw/LivechatDepartment.ts b/apps/meteor/server/models/raw/LivechatDepartment.ts index 96a0dc5c9e0e..3d06a2b0bae9 100644 --- a/apps/meteor/server/models/raw/LivechatDepartment.ts +++ b/apps/meteor/server/models/raw/LivechatDepartment.ts @@ -169,6 +169,10 @@ export class LivechatDepartmentRaw extends BaseRaw implemen return this.find(query, options); } + findOneEnabledWithAgentsAndAvailableOnRegistration(): Promise { + return this.findOne({ enabled: true, numAgents: { $gt: 0 }, showOnRegistration: true }); + } + addBusinessHourToDepartmentsByIds(ids: string[] = [], businessHourId: string): Promise { const query = { _id: { $in: ids }, @@ -288,6 +292,17 @@ export class LivechatDepartmentRaw extends BaseRaw implemen return this.find(query, projection && { projection }); } + findEnabledWithAgentsAndAvailableOnRegistration( + projection: FindOptions['projection'] = {}, + ): FindCursor { + const query = { + numAgents: { $gt: 0 }, + enabled: true, + showOnRegistration: true, + }; + return this.find(query, projection && { projection }); + } + async findEnabledWithAgentsAndBusinessUnit( _: any, projection: FindOptions['projection'] = {}, diff --git a/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts b/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts index 91f3f4e22e34..c8f529d025a1 100644 --- a/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts +++ b/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts @@ -287,6 +287,19 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw { + const agents = await this.findByDepartmentId(departmentId).toArray(); + + if (agents.length === 0) { + return 0; + } + + return Users.countOnlineAgentsFromList( + agents.map((a) => a.username), + isLivechatEnabledWhenAgentIdle, + ); + } + async getBotsForDepartment(departmentId: string): Promise> { const agents = await this.findByDepartmentId(departmentId).toArray(); diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 0d56fc76bccf..6264dc938cb6 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -1425,6 +1425,12 @@ export class UsersRaw extends BaseRaw { return this.find(query); } + countOnlineAgentsFromList(userList, isLivechatEnabledWhenAgentIdle) { + const query = queryStatusAgentOnline({ username: { $in: [].concat(userList) } }, isLivechatEnabledWhenAgentIdle); + + return this.countDocuments(query); + } + findOneOnlineAgentByUserList(userList, options, isLivechatEnabledWhenAgentIdle) { // TODO:: Create class Agent const username = { diff --git a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts index 7d8f8eda0ef4..5e432cf9e4b9 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts @@ -95,4 +95,5 @@ export interface ILivechatDepartmentAgentsModel extends IBaseModel; enableAgentsByDepartmentId(departmentId: string): Promise; findAllAgentsConnectedToListOfDepartments(departmentIds: string[]): Promise; + countOnlineForDepartment(departmentId: string, isLivechatEnabledWhenAgentIdle?: boolean): Promise; } diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index 75fe0f54b2eb..91e434220a08 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -24,6 +24,8 @@ export interface ILivechatDepartmentModel extends IBaseModel, ): FindCursor; + findOneEnabledWithAgentsAndAvailableOnRegistration(): Promise; + findActiveDepartmentsWithoutBusinessHour(options: FindOptions): FindCursor; addBusinessHourToDepartmentsByIds(ids: string[], businessHourId: string): Promise; @@ -53,6 +55,9 @@ export interface ILivechatDepartmentModel extends IBaseModel; decreaseNumberOfAgentsByIds(_ids: string[]): Promise; findEnabledWithAgents(projection?: FindOptions['projection']): FindCursor; + findEnabledWithAgentsAndAvailableOnRegistration( + projection?: FindOptions['projection'], + ): FindCursor; findEnabledWithAgentsAndBusinessUnit( _: any, projection: FindOptions['projection'], diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index f9a2b1c45a2a..fb77984c6b05 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -216,6 +216,7 @@ export interface IUsersModel extends IBaseModel { countFederatedExternalUsers(): Promise; findOnlineUserFromList(userList: string[], isLivechatEnabledWhenAgentIdle?: boolean): FindCursor; + countOnlineAgentsFromList(userList: string[], isLivechatEnabledWhenAgentIdle?: boolean): Promise; getUnavailableAgents( departmentId?: string, extraQuery?: Document, From 756a5eee401753f4864b238e6488cfa8b2ca8659 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 8 Jan 2024 10:48:18 -0600 Subject: [PATCH 3/8] optimize createRoom --- apps/meteor/app/livechat/server/lib/Helper.ts | 37 +++---------------- .../app/livechat/server/lib/QueueManager.ts | 6 ++- .../server/hooks/beforeNewRoom.ts | 2 +- apps/meteor/lib/callbacks.ts | 1 - 4 files changed, 10 insertions(+), 36 deletions(-) diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index dbf21fa938cb..450b0b1d9a12 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -60,18 +60,6 @@ export const createLivechatRoom = async ( roomInfo: Partial = {}, extraData = {}, ) => { - check(rid, String); - check(name, String); - check( - guest, - Match.ObjectIncluding({ - _id: String, - username: String, - status: Match.Maybe(String), - department: Match.Maybe(String), - }), - ); - const extraRoomInfo = await callbacks.run('livechat.beforeRoom', roomInfo, extraData); const { _id, username, token, department: departmentId, status = 'online' } = guest; const newRoomAt = new Date(); @@ -134,33 +122,18 @@ export const createLivechatInquiry = async ({ extraData, }: { rid: string; - name?: string; + name: string; guest?: Pick; message?: Pick; initialStatus?: LivechatInquiryStatus; extraData?: Pick; }) => { - check(rid, String); - check(name, String); - check( - guest, - Match.ObjectIncluding({ - _id: String, - username: String, - status: Match.Maybe(String), - department: Match.Maybe(String), - activity: Match.Maybe([String]), - }), - ); - check( - message, - Match.ObjectIncluding({ - msg: String, - }), - ); - const extraInquiryInfo = await callbacks.run('livechat.beforeInquiry', extraData); + if (!guest || !message) { + throw new Meteor.Error('error-invalid-params'); + } + const { _id, username, token, department, status = UserStatus.ONLINE, activity } = guest; const { msg } = message; const ts = new Date(); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index fe66efbc6f17..e904155164d0 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -82,7 +82,7 @@ export const QueueManager: queueManager = { const { rid } = message; const name = (roomInfo?.fname as string) || guest.name || guest.username; - const room = await LivechatRooms.findOneById(await createLivechatRoom(rid, name, guest, roomInfo, extraData)); + const room = await createLivechatRoom(rid, name, guest, roomInfo, extraData); if (!room) { throw new Error('room-not-found'); } @@ -156,7 +156,9 @@ export const QueueManager: queueManager = { if (!room) { throw new Error('room-not-found'); } - const inquiry = await LivechatInquiry.findOneById(await createLivechatInquiry({ rid, name, guest, message, extraData: { source } })); + const inquiry = await LivechatInquiry.findOneById( + await createLivechatInquiry({ rid, name: name || guest.username, guest, message, extraData: { source } }), + ); if (!inquiry) { throw new Error('inquiry-not-found'); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewRoom.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewRoom.ts index 35219fc6e03b..71aa9b8a3943 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewRoom.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewRoom.ts @@ -15,7 +15,7 @@ callbacks.add( return roomInfo; } - const sla = await OmnichannelServiceLevelAgreements.findOneByIdOrName(searchTerm); + const sla = await OmnichannelServiceLevelAgreements.findOneByIdOrName(searchTerm, { projection: { _id: 1 } }); if (!sla) { throw new Meteor.Error('error-invalid-sla', 'Invalid sla', { function: 'livechat.beforeRoom', diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 51d270fefaee..9e3c9eb58edf 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -213,7 +213,6 @@ export type Hook = | 'beforeRemoveFromRoom' | 'beforeValidateLogin' | 'livechat.beforeForwardRoomToDepartment' - | 'livechat.beforeRoom' | 'livechat.beforeRouteChat' | 'livechat.chatQueued' | 'livechat.checkAgentBeforeTakeInquiry' From 90fb6ab319203484b4e1684b2db06303c67109b9 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 8 Jan 2024 12:04:22 -0600 Subject: [PATCH 4/8] pass room to takeinquiry --- apps/meteor/app/livechat/server/lib/Helper.ts | 4 ++-- .../app/livechat/server/lib/RoutingManager.ts | 24 +++++++++++-------- .../livechat/server/methods/takeInquiry.ts | 2 +- .../server/methods/resumeOnHold.ts | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 450b0b1d9a12..768a88add928 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -406,7 +406,7 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T // There are some Enterprise features that may interrupt the forwarding process // Due to that we need to check whether the agent has been changed or not logger.debug(`Forwarding inquiry ${inquiry._id} to agent ${agent.agentId}`); - const roomTaken = await RoutingManager.takeInquiry(inquiry, agent, { + const roomTaken = await RoutingManager.takeInquiry(inquiry, agent, room, { ...(clientAction && { clientAction }), }); if (!roomTaken) { @@ -519,7 +519,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi // Fake the department to forward the inquiry - Case the forward process does not success // the inquiry will stay in the same original department inquiry.department = departmentId; - const roomTaken = await RoutingManager.delegateInquiry(inquiry, agent, { + const roomTaken = await RoutingManager.delegateInquiry(inquiry, agent, room, { forwardingToDepartment: { oldDepartmentId }, ...(clientAction && { clientAction }), }); diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index cadbadf500b3..58331c4771f2 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -45,6 +45,7 @@ type Routing = { delegateInquiry( inquiry: InquiryWithAgentInfo, agent?: SelectedAgent | null, + room?: IOmnichannelRoom | null, options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any } }, ): Promise<(IOmnichannelRoom & { chatQueued?: boolean }) | null | void>; assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent, room: IOmnichannelRoom | null): Promise; @@ -55,6 +56,7 @@ type Routing = { 'estimatedInactivityCloseTimeAt' | 'message' | 't' | 'source' | 'estimatedWaitingTimeQueue' | 'priorityWeight' | '_updatedAt' >, agent: SelectedAgent | null, + room: IOmnichannelRoom | null, options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any } }, ): Promise; transferRoom(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData): Promise; @@ -119,7 +121,7 @@ export const RoutingManager: Routing = { return this.getMethod().getNextAgent(department, ignoreAgentId); }, - async delegateInquiry(inquiry, agent, options = {}) { + async delegateInquiry(inquiry, agent, room, options = {}) { const { department, rid } = inquiry; logger.debug(`Attempting to delegate inquiry ${inquiry._id}`); if ( @@ -145,8 +147,12 @@ export const RoutingManager: Routing = { return LivechatRooms.findOneById(rid); } + if (!room) { + return LivechatRooms.findOneById(rid); + } + logger.debug(`Inquiry ${inquiry._id} will be taken by agent ${agent.agentId}`); - return this.takeInquiry(inquiry, agent, options); + return this.takeInquiry(inquiry, agent, room, options); }, async assignAgent(inquiry, agent, room) { @@ -165,17 +171,16 @@ export const RoutingManager: Routing = { throw new Meteor.Error('error-creating-subscription', 'Error creating subscription'); } - await Promise.all([LivechatRooms.changeAgentByRoomId(rid, agent), Rooms.incUsersCountById(rid, 1)]); - const user = await Users.findOneById(agent.agentId); + const [user] = await Promise.all([ + Users.findOneById(agent.agentId), + LivechatRooms.changeAgentByRoomId(rid, agent), + Rooms.incUsersCountById(rid, 1), + ]); if (user) { await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]); } - if (!room) { - throw new Meteor.Error('error-room-not-found', 'Room not found'); - } - void dispatchAgentDelegated(rid, agent.agentId); void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); @@ -215,7 +220,7 @@ export const RoutingManager: Routing = { return true; }, - async takeInquiry(inquiry, agent, options = { clientAction: false }) { + async takeInquiry(inquiry, agent, room, options = { clientAction: false }) { check( agent, Match.ObjectIncluding({ @@ -236,7 +241,6 @@ export const RoutingManager: Routing = { logger.debug(`Attempting to take Inquiry ${inquiry._id} [Agent ${agent.agentId}] `); const { _id, rid } = inquiry; - const room = await LivechatRooms.findOneById(rid); if (!room?.open) { logger.debug(`Cannot take Inquiry ${inquiry._id}: Room is closed`); return room; diff --git a/apps/meteor/app/livechat/server/methods/takeInquiry.ts b/apps/meteor/app/livechat/server/methods/takeInquiry.ts index 3433b4a33ae8..85f4ed7c1a08 100644 --- a/apps/meteor/app/livechat/server/methods/takeInquiry.ts +++ b/apps/meteor/app/livechat/server/methods/takeInquiry.ts @@ -60,7 +60,7 @@ export const takeInquiry = async ( }; try { - await RoutingManager.takeInquiry(inquiry, agent, options); + await RoutingManager.takeInquiry(inquiry, agent, room, options); } catch (e: any) { throw new Meteor.Error(e.message); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index c3e6d434cf7f..82ff45b93844 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -73,7 +73,7 @@ Meteor.methods({ const { servedBy: { _id: agentId, username }, } = room; - await RoutingManager.takeInquiry(inquiry, { agentId, username }, options); + await RoutingManager.takeInquiry(inquiry, { agentId, username }, room, options); const onHoldChatResumedBy = options.clientAction ? await Meteor.userAsync() : await Users.findOneById('rocket.cat'); if (!onHoldChatResumedBy) { From 622e62b8031acd94f505219ea456dd653e6685d1 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 8 Jan 2024 14:46:49 -0600 Subject: [PATCH 5/8] other optimizations --- apps/meteor/app/livechat/server/lib/Helper.ts | 8 +-- .../app/livechat/server/lib/QueueManager.ts | 57 +++++++++++++------ .../app/livechat/server/lib/RoutingManager.ts | 18 +++--- .../services/omnichannel.internalService.ts | 4 +- .../models/raw/LivechatDepartmentAgents.ts | 10 ++-- .../meteor/server/models/raw/LivechatRooms.ts | 18 ------ apps/meteor/server/models/raw/Users.js | 14 +++++ .../server/services/omnichannel/queue.ts | 4 +- .../src/models/ILivechatRoomsModel.ts | 10 +--- .../model-typings/src/models/IUsersModel.ts | 5 ++ 10 files changed, 85 insertions(+), 63 deletions(-) diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 768a88add928..8cfe7d510af9 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -561,16 +561,16 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi if (chatQueued) { logger.debug(`Forwarding succesful. Marking inquiry ${inquiry._id} as ready`); - await LivechatInquiry.readyInquiry(inquiry._id); - await LivechatRooms.removeAgentByRoomId(rid); - await dispatchAgentDelegated(rid); + await Promise.all([LivechatInquiry.readyInquiry(inquiry._id), LivechatRooms.removeAgentByRoomId(rid)]); + void dispatchAgentDelegated(rid); + const newInquiry = await LivechatInquiry.findOneById(inquiry._id); if (!newInquiry) { logger.debug(`Inquiry ${inquiry._id} not found`); throw new Error('error-invalid-inquiry'); } - await queueInquiry(newInquiry); + await queueInquiry(newInquiry, room); logger.debug(`Inquiry ${inquiry._id} queued succesfully`); } diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index e904155164d0..227107086709 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -1,7 +1,14 @@ import { Omnichannel } from '@rocket.chat/core-services'; -import type { ILivechatInquiryRecord, ILivechatVisitor, IMessage, IOmnichannelRoom, SelectedAgent } from '@rocket.chat/core-typings'; +import type { + ILivechatInquiryRecord, + ILivechatVisitor, + IMessage, + IOmnichannelRoom, + ISetting, + SelectedAgent, +} from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; -import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; +import { LivechatInquiry, LivechatRooms, Users, Settings } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -16,7 +23,7 @@ export const saveQueueInquiry = async (inquiry: ILivechatInquiryRecord) => { await callbacks.run('livechat.afterInquiryQueued', inquiry); }; -export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent?: SelectedAgent) => { +export const queueInquiry = async (inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, defaultAgent?: SelectedAgent) => { const inquiryAgent = await RoutingManager.delegateAgent(defaultAgent, inquiry); logger.debug({ msg: 'Routing inquiry', @@ -25,7 +32,6 @@ export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent }); const dbInquiry = await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent); - const room = await LivechatRooms.findOneById(inquiry.rid, { projection: { v: 1 } }); if (!room || !(await Omnichannel.isWithinMACLimit(room))) { logger.error({ msg: 'MAC limit reached, not routing inquiry', inquiry }); // We'll queue these inquiries so when new license is applied, they just start rolling again @@ -36,7 +42,7 @@ export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent if (dbInquiry.status === 'ready') { logger.debug(`Inquiry with id ${inquiry._id} is ready. Delegating to agent ${inquiryAgent?.username}`); - return RoutingManager.delegateInquiry(dbInquiry, inquiryAgent); + return RoutingManager.delegateInquiry(dbInquiry, inquiryAgent, room); } }; @@ -52,9 +58,22 @@ type queueManager = { extraData?: Record; }) => Promise; unarchiveRoom: (archivedRoom?: IOmnichannelRoom) => Promise; + updateRoomCount: () => Promise; }; export const QueueManager: queueManager = { + async updateRoomCount() { + const livechatCount = await Settings.findOneAndUpdate( + { + _id: 'Livechat_Room_Count', + }, + // @ts-expect-error - Caused by `OnlyFieldsOfType` on mongo which excludes `SettingValue` from $inc + { $inc: { value: 1 } }, + { returnDocument: 'after' }, + ); + + return livechatCount.value; + }, async requestRoom({ guest, message, roomInfo, agent, extraData }) { logger.debug(`Requesting a room for guest ${guest._id}`); check( @@ -82,20 +101,23 @@ export const QueueManager: queueManager = { const { rid } = message; const name = (roomInfo?.fname as string) || guest.name || guest.username; - const room = await createLivechatRoom(rid, name, guest, roomInfo, extraData); - if (!room) { - throw new Error('room-not-found'); - } - - const inquiry = await LivechatInquiry.findOneById( - await createLivechatInquiry({ + const [, inquiryId] = await Promise.all([ + createLivechatRoom(rid, name, guest, roomInfo, extraData), + createLivechatInquiry({ rid, name, guest, message, extraData: { ...extraData, source: roomInfo.source }, }), - ); + ]); + + const [inquiry, dbRoom] = await Promise.all([ + LivechatInquiry.findOneById(inquiryId), + LivechatRooms.findOneById(rid), + this.updateRoomCount(), + ]); + if (!inquiry) { throw new Error('inquiry-not-found'); } @@ -106,14 +128,17 @@ export const QueueManager: queueManager = { inquiryId: inquiry._id, }); - await LivechatRooms.updateRoomCount(); + if (!dbRoom) { + throw new Error('room-not-found'); + } - await queueInquiry(inquiry, agent); + await queueInquiry(inquiry, dbRoom, agent); logger.debug({ msg: 'Inquiry queued', inquiryId: inquiry._id, }); + // After all, we need the fresh room :) const newRoom = await LivechatRooms.findOneById(rid); if (!newRoom) { throw new Error('room-not-found'); @@ -163,7 +188,7 @@ export const QueueManager: queueManager = { throw new Error('inquiry-not-found'); } - await queueInquiry(inquiry, defaultAgent); + await queueInquiry(inquiry, room, defaultAgent); logger.debug(`Inquiry ${inquiry._id} queued`); return room; diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index 58331c4771f2..57b1cc084cab 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -48,7 +48,7 @@ type Routing = { room?: IOmnichannelRoom | null, options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any } }, ): Promise<(IOmnichannelRoom & { chatQueued?: boolean }) | null | void>; - assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent, room: IOmnichannelRoom | null): Promise; + assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent): Promise; unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string): Promise; takeInquiry( inquiry: Omit< @@ -122,7 +122,7 @@ export const RoutingManager: Routing = { }, async delegateInquiry(inquiry, agent, room, options = {}) { - const { department, rid } = inquiry; + const { department } = inquiry; logger.debug(`Attempting to delegate inquiry ${inquiry._id}`); if ( !agent || @@ -144,18 +144,18 @@ export const RoutingManager: Routing = { logger.debug(`No agents available. Unable to delegate inquiry ${inquiry._id}`); // When an inqury reaches here on CE, it will stay here as 'ready' since on CE there's no mechanism to re queue it. // When reaching this point, managers have to manually transfer the inquiry to another room. This is expected. - return LivechatRooms.findOneById(rid); + return room; } if (!room) { - return LivechatRooms.findOneById(rid); + return null; } logger.debug(`Inquiry ${inquiry._id} will be taken by agent ${agent.agentId}`); return this.takeInquiry(inquiry, agent, room, options); }, - async assignAgent(inquiry, agent, room) { + async assignAgent(inquiry, agent) { check( agent, Match.ObjectIncluding({ @@ -181,6 +181,11 @@ export const RoutingManager: Routing = { await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]); } + const room = await LivechatRooms.findOneById(rid); + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'assignAgent' }); + } + void dispatchAgentDelegated(rid, agent.agentId); void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); @@ -272,8 +277,7 @@ export const RoutingManager: Routing = { }); } - await LivechatInquiry.takeInquiry(_id); - const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, agent, room); + const [, inq] = await Promise.all([LivechatInquiry.takeInquiry(_id), this.assignAgent(inquiry as InquiryWithAgentInfo, agent)]); logger.info(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); callbacks.runAsync('livechat.afterTakeInquiry', inq, agent); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts index 0d6c42d769ee..68709741319e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts @@ -116,7 +116,7 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE servingAgent, clientAction, }: { - room: Pick; + room: IOmnichannelRoom; inquiry: ILivechatInquiryRecord; servingAgent: NonNullable; clientAction: boolean; @@ -151,7 +151,7 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE if (!newInquiry) { throw new Error('error-invalid-inquiry'); } - await queueInquiry(newInquiry); + await queueInquiry(newInquiry, room); } private async removeCurrentAgentFromRoom({ diff --git a/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts b/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts index c8f529d025a1..82749a6959e3 100644 --- a/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts +++ b/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts @@ -208,21 +208,21 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw u.username); + + const onlineUsers = await Users.findOnlineUsersFromListExcludingUsernames( agents.map((agent) => agent.username), + currentUnavailableAgents, isLivechatEnabledWhenAgentIdle, ).toArray(); const onlineUsernames = onlineUsers.map((user) => user.username).filter(isStringValue); - // get fully booked agents, to ignore them from the query - const currentUnavailableAgents = (await Users.getUnavailableAgents(departmentId, extraQuery)).map((u) => u.username); - const query: Filter = { departmentId, username: { $in: onlineUsernames, - $nin: currentUnavailableAgents, }, ...(ignoreAgentId && { agentId: { $ne: ignoreAgentId } }), }; diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index 1423476a708b..7d15508270b7 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -3,7 +3,6 @@ import type { RocketChatRecordDeleted, IOmnichannelRoomClosingInfo, DeepWritable, - ISetting, IMessage, ILivechatPriority, IOmnichannelServiceLevelAgreements, @@ -12,7 +11,6 @@ import type { } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import type { ILivechatRoomsModel } from '@rocket.chat/model-typings'; -import { Settings } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { Db, @@ -1852,22 +1850,6 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.findOne(query, options); } - async updateRoomCount() { - const query: Filter = { - _id: 'Livechat_Room_Count', - }; - - const update: UpdateFilter = { - $inc: { - // @ts-expect-error - Caused by `OnlyFieldsOfType` on mongo which excludes `SettingValue` from $inc - value: 1, - }, - }; - - const livechatCount = await Settings.findOneAndUpdate(query, update, { returnDocument: 'after' }); - return livechatCount.value; - } - findOpenByVisitorToken(visitorToken: string, options: FindOptions = {}, extraQuery: Filter = {}) { const query: Filter = { 't': 'l', diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 6264dc938cb6..b36dd1845c5c 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -1425,6 +1425,20 @@ export class UsersRaw extends BaseRaw { return this.find(query); } + findOnlineUsersFromListExcludingUsernames(userList, usernames, isLivechatEnabledWhenAgentIdle) { + return this.find( + queryStatusAgentOnline( + { + username: { + $in: userList, + $nin: usernames, + }, + }, + isLivechatEnabledWhenAgentIdle, + ), + ); + } + countOnlineAgentsFromList(userList, isLivechatEnabledWhenAgentIdle) { const query = queryStatusAgentOnline({ username: { $in: [].concat(userList) } }, isLivechatEnabledWhenAgentIdle); diff --git a/apps/meteor/server/services/omnichannel/queue.ts b/apps/meteor/server/services/omnichannel/queue.ts index 603c5197ed30..0f6fbd342f52 100644 --- a/apps/meteor/server/services/omnichannel/queue.ts +++ b/apps/meteor/server/services/omnichannel/queue.ts @@ -1,6 +1,6 @@ import type { InquiryWithAgentInfo, IOmnichannelQueue } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; -import { LivechatInquiry } from '@rocket.chat/models'; +import { LivechatInquiry, LivechatRooms } from '@rocket.chat/models'; import { dispatchAgentDelegated } from '../../../app/livechat/server/lib/Helper'; import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'; @@ -129,7 +129,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { queueLogger.debug(`Processing inquiry ${inquiry._id} from queue ${queue}`); const { defaultAgent } = inquiry; - const room = await RoutingManager.delegateInquiry(inquiry, defaultAgent); + const room = await RoutingManager.delegateInquiry(inquiry, defaultAgent, await LivechatRooms.findOneById(inquiry.rid)); const propagateAgentDelegated = async (rid: string, agentId: string) => { await dispatchAgentDelegated(rid, agentId); diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index a228a4fea864..1bdbb86691b2 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -1,11 +1,4 @@ -import type { - IMessage, - IOmnichannelRoom, - IOmnichannelRoomClosingInfo, - ISetting, - ILivechatVisitor, - MACStats, -} from '@rocket.chat/core-typings'; +import type { IMessage, IOmnichannelRoom, IOmnichannelRoomClosingInfo, ILivechatVisitor, MACStats } from '@rocket.chat/core-typings'; import type { FindCursor, UpdateResult, AggregationCursor, Document, FindOptions, DeleteResult, Filter } from 'mongodb'; import type { FindPaginated } from '..'; @@ -167,7 +160,6 @@ export interface ILivechatRoomsModel extends IBaseModel { updateEmailThreadByRoomId(roomId: string, threadIds: string[] | string): Promise; findOneLastServedAndClosedByVisitorToken(visitorToken: string, options?: FindOptions): Promise; findOneByVisitorToken(visitorToken: string, fields?: FindOptions['projection']): Promise; - updateRoomCount(): Promise; findOpenByVisitorToken( visitorToken: string, options?: FindOptions, diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index fb77984c6b05..0f2ef1308499 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -216,6 +216,11 @@ export interface IUsersModel extends IBaseModel { countFederatedExternalUsers(): Promise; findOnlineUserFromList(userList: string[], isLivechatEnabledWhenAgentIdle?: boolean): FindCursor; + findOnlineUsersFromListExcludingUsernames( + userList: string[], + usernames: string[], + isLivechatEnabledWhenAgentIdle?: boolean, + ): FindCursor; countOnlineAgentsFromList(userList: string[], isLivechatEnabledWhenAgentIdle?: boolean): Promise; getUnavailableAgents( departmentId?: string, From 2e6086025e034df75e90a64dcee12c8bea16f1ea Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 9 Jan 2024 09:36:06 -0600 Subject: [PATCH 6/8] ignre internal setting from event --- apps/meteor/server/modules/listeners/listeners.module.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index ae78f78b0e39..b27802c64993 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -275,6 +275,11 @@ export class ListenersModule { return; } + if (setting._id === 'Livechat_Room_Count') { + // Ignore this setting, it's only used internally + return; + } + const value = { _id: setting._id, value: setting.value, From f5c984ff300d0e42483fe1c376721ca8f8e40d12 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 9 Jan 2024 09:47:54 -0600 Subject: [PATCH 7/8] useless find --- .../meteor/app/livechat/server/lib/QueueManager.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 227107086709..b6fb660f8dde 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -122,24 +122,22 @@ export const QueueManager: queueManager = { throw new Error('inquiry-not-found'); } + if (!dbRoom) { + throw new Error('room-not-found'); + } + logger.debug({ msg: 'New room created', rid, inquiryId: inquiry._id, }); - if (!dbRoom) { - throw new Error('room-not-found'); - } - - await queueInquiry(inquiry, dbRoom, agent); + const newRoom = await queueInquiry(inquiry, dbRoom, agent); logger.debug({ msg: 'Inquiry queued', inquiryId: inquiry._id, }); - // After all, we need the fresh room :) - const newRoom = await LivechatRooms.findOneById(rid); if (!newRoom) { throw new Error('room-not-found'); } @@ -172,7 +170,7 @@ export const QueueManager: queueManager = { }; let defaultAgent: SelectedAgent | undefined; - if (servedBy?.username && (await Users.findOneOnlineAgentByUserList(servedBy.username))) { + if (servedBy?.username && (await Users.findOneOnlineAgentByUserList(servedBy.username, { projection: { _id: 1 } }))) { defaultAgent = { agentId: servedBy._id, username: servedBy.username }; } From abd41fa7ad2fd1eca66e65cba8125766740dd53e Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 9 Jan 2024 12:08:04 -0600 Subject: [PATCH 8/8] minor --- .../app/livechat/server/lib/LivechatTyped.ts | 26 +++++++----- .../app/livechat/server/lib/QueueManager.ts | 40 ++++++++----------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index a823ebe29ed5..d4b4947e4f1e 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -397,6 +397,21 @@ class LivechatClass { }); } + async validateGuestDepartment(guest: ILivechatVisitor) { + if ( + guest.department && + !(await LivechatDepartment.findOneById>(guest.department, { projection: { _id: 1 } })) + ) { + await LivechatVisitors.removeDepartmentById(guest._id); + const tmpGuest = await LivechatVisitors.findOneEnabledById(guest._id); + if (tmpGuest) { + guest = tmpGuest; + } + } + + return guest; + } + async getRoom( guest: ILivechatVisitor, message: Pick, @@ -429,16 +444,7 @@ class LivechatClass { }); } - if ( - guest.department && - !(await LivechatDepartment.findOneById>(guest.department, { projection: { _id: 1 } })) - ) { - await LivechatVisitors.removeDepartmentById(guest._id); - const tmpGuest = await LivechatVisitors.findOneEnabledById(guest._id); - if (tmpGuest) { - guest = tmpGuest; - } - } + guest = await this.validateGuestDepartment(guest); if (room == null) { room = await this.getNewRoom(guest, message, roomInfo, agent, extraData); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index b6fb660f8dde..0b5259bd0442 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -1,12 +1,5 @@ import { Omnichannel } from '@rocket.chat/core-services'; -import type { - ILivechatInquiryRecord, - ILivechatVisitor, - IMessage, - IOmnichannelRoom, - ISetting, - SelectedAgent, -} from '@rocket.chat/core-typings'; +import type { ILivechatInquiryRecord, ILivechatVisitor, IMessage, IOmnichannelRoom, SelectedAgent } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatInquiry, LivechatRooms, Users, Settings } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -20,7 +13,7 @@ const logger = new Logger('QueueManager'); export const saveQueueInquiry = async (inquiry: ILivechatInquiryRecord) => { await LivechatInquiry.queueInquiry(inquiry._id); - await callbacks.run('livechat.afterInquiryQueued', inquiry); + void callbacks.run('livechat.afterInquiryQueued', inquiry); }; export const queueInquiry = async (inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, defaultAgent?: SelectedAgent) => { @@ -46,6 +39,19 @@ export const queueInquiry = async (inquiry: ILivechatInquiryRecord, room: IOmnic } }; +async function updateRoomCount() { + const livechatCount = await Settings.findOneAndUpdate( + { + _id: 'Livechat_Room_Count', + }, + // @ts-expect-error - Caused by `OnlyFieldsOfType` on mongo which excludes `SettingValue` from $inc + { $inc: { value: 1 } }, + { returnDocument: 'after' }, + ); + + return livechatCount.value; +} + type queueManager = { requestRoom: (params: { guest: ILivechatVisitor; @@ -58,24 +64,10 @@ type queueManager = { extraData?: Record; }) => Promise; unarchiveRoom: (archivedRoom?: IOmnichannelRoom) => Promise; - updateRoomCount: () => Promise; }; export const QueueManager: queueManager = { - async updateRoomCount() { - const livechatCount = await Settings.findOneAndUpdate( - { - _id: 'Livechat_Room_Count', - }, - // @ts-expect-error - Caused by `OnlyFieldsOfType` on mongo which excludes `SettingValue` from $inc - { $inc: { value: 1 } }, - { returnDocument: 'after' }, - ); - - return livechatCount.value; - }, async requestRoom({ guest, message, roomInfo, agent, extraData }) { - logger.debug(`Requesting a room for guest ${guest._id}`); check( message, Match.ObjectIncluding({ @@ -115,7 +107,7 @@ export const QueueManager: queueManager = { const [inquiry, dbRoom] = await Promise.all([ LivechatInquiry.findOneById(inquiryId), LivechatRooms.findOneById(rid), - this.updateRoomCount(), + updateRoomCount(), ]); if (!inquiry) {