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..8cfe7d510af9 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(); @@ -117,11 +105,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; }; @@ -135,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(); @@ -193,7 +165,6 @@ export const createLivechatInquiry = async ({ }; const result = (await LivechatInquiry.insertOne(inquiry)).insertedId; - logger.debug(`Inquiry ${result} created for visitor ${_id}`); return result; }; @@ -338,7 +309,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 +328,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) { @@ -436,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) { @@ -549,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 }), }); @@ -591,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/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index ea508b047882..d4b4947e4f1e 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -343,24 +343,23 @@ 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; } } } - async getRoom( + async getNewRoom( guest: ILivechatVisitor, message: Pick, roomInfo: { @@ -370,19 +369,35 @@ class LivechatClass { agent?: SelectedAgent, extraData?: Record, ) { - if (!this.enabled()) { - throw new Meteor.Error('error-omnichannel-is-disabled'); + 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; + } } - Livechat.logger.debug(`Attempting to find or create a room for visitor ${guest._id}`); - 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; - } + // 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 validateGuestDepartment(guest: ILivechatVisitor) { if ( guest.department && !(await LivechatDepartment.findOneById>(guest.department, { projection: { _id: 1 } })) @@ -394,42 +409,58 @@ 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; - } - } + return guest; + } + + async getRoom( + guest: ILivechatVisitor, + message: Pick, + roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + }, + agent?: SelectedAgent, + extraData?: Record, + ) { + if (!this.enabled()) { + throw new Meteor.Error('error-omnichannel-is-disabled'); + } + + this.logger.debug({ + msg: 'Getting room for visitor', + visitor: guest, + }); + + let room = await LivechatRooms.findOneById(message.rid); + let newRoom = false; - // 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, + if (room && !room.open) { + message.rid = Random.id(); + room = null; + + this.logger.debug({ + msg: 'Room is closed. Opening new one', + room: message.rid, }); - newRoom = true; + } + + guest = await this.validateGuestDepartment(guest); - Livechat.logger.debug(`Room obtained for visitor ${guest._id} -> ${room._id}`); + if (room == null) { + room = await this.getNewRoom(guest, message, roomInfo, agent, extraData); + await Messages.setRoomIdByToken(guest.token, room._id); + newRoom = true; } 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..0b5259bd0442 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -1,7 +1,7 @@ import { Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatInquiryRecord, ILivechatVisitor, IMessage, IOmnichannelRoom, 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'; @@ -13,15 +13,18 @@ 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, defaultAgent?: SelectedAgent) => { +export const queueInquiry = async (inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, 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 room = await LivechatRooms.findOneById(inquiry.rid, { projection: { v: 1 } }); + const dbInquiry = await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent); 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 @@ -29,18 +32,26 @@ 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}`); - return RoutingManager.delegateInquiry(dbInquiry, inquiryAgent); + return RoutingManager.delegateInquiry(dbInquiry, inquiryAgent, room); } }; +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; @@ -57,7 +68,6 @@ type queueManager = { export const QueueManager: queueManager = { async requestRoom({ guest, message, roomInfo, agent, extraData }) { - logger.debug(`Requesting a room for guest ${guest._id}`); check( message, Match.ObjectIncluding({ @@ -83,35 +93,44 @@ 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)); - 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({ + 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), + updateRoomCount(), + ]); + if (!inquiry) { - logger.error(`Inquiry for visitor ${guest._id} not found`); throw new Error('inquiry-not-found'); } - await LivechatRooms.updateRoomCount(); + if (!dbRoom) { + throw new Error('room-not-found'); + } - await queueInquiry(inquiry, agent); - logger.debug(`Inquiry ${inquiry._id} queued`); + logger.debug({ + msg: 'New room created', + rid, + inquiryId: inquiry._id, + }); + + const newRoom = await queueInquiry(inquiry, dbRoom, agent); + 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'); } @@ -143,7 +162,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 }; } @@ -152,12 +171,14 @@ 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'); } - 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 7b85c31f26ac..57b1cc084cab 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, @@ -44,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): Promise; @@ -54,11 +56,13 @@ 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; 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 = { @@ -117,24 +121,38 @@ export const RoutingManager: Routing = { return this.getMethod().getNextAgent(department, ignoreAgentId); }, - async delegateInquiry(inquiry, agent, options = {}) { - const { department, rid } = inquiry; + async delegateInquiry(inquiry, agent, room, options = {}) { + const { department } = 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) { 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 null; } 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) { @@ -150,29 +168,27 @@ 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); - - const user = await Users.findOneById(agent.agentId); - const room = await LivechatRooms.findOneById(rid); + 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)]); } + const room = await LivechatRooms.findOneById(rid); 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'); + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'assignAgent' }); } - 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; }, @@ -209,7 +225,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({ @@ -230,7 +246,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; @@ -256,15 +271,13 @@ 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 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); @@ -288,18 +301,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/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/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/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/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) { 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/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index dcaaf3d15ae3..9e3c9eb58edf 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'] }, @@ -214,7 +213,6 @@ export type Hook = | 'beforeRemoveFromRoom' | 'beforeValidateLogin' | 'livechat.beforeForwardRoomToDepartment' - | 'livechat.beforeRoom' | 'livechat.beforeRouteChat' | 'livechat.chatQueued' | 'livechat.checkAgentBeforeTakeInquiry' 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..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 } }), }; @@ -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/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 0d56fc76bccf..b36dd1845c5c 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -1425,6 +1425,26 @@ 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); + + return this.countDocuments(query); + } + findOneOnlineAgentByUserList(userList, options, isLivechatEnabledWhenAgentIdle) { // TODO:: Create class Agent const username = { 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, 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/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(); 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/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 f9a2b1c45a2a..0f2ef1308499 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -216,6 +216,12 @@ 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, extraQuery?: Document,