From bc33bf5ab7cdf4412ea191cb7809fb54720131db Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 23 Jun 2023 00:35:52 -0600 Subject: [PATCH] refactor: convert routing manager to ts (#29406) --- .../app/livechat/server/api/lib/livechat.ts | 2 +- .../server/hooks/saveLastMessageToInquiry.ts | 2 +- .../{RoutingManager.js => RoutingManager.ts} | 81 ++++++++++++++++--- .../server/methods/getRoutingConfig.ts | 2 +- .../livechat/server/methods/takeInquiry.ts | 7 +- .../roomAccessValidator.compatibility.ts | 3 +- .../hooks/handleNextAgentPreferredEvents.ts | 4 +- .../server/lib/AutoTransferChatScheduler.ts | 2 +- .../livechat-enterprise/server/lib/Helper.ts | 1 + .../server/lib/LivechatEnterprise.ts | 2 +- apps/meteor/lib/callbacks.ts | 17 +--- packages/core-typings/src/IInquiry.ts | 7 +- .../core-typings/src/omnichannel/routing.ts | 1 + 13 files changed, 90 insertions(+), 41 deletions(-) rename apps/meteor/app/livechat/server/lib/{RoutingManager.js => RoutingManager.ts} (71%) diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index 1629685a3e1..8f2719f8855 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -126,7 +126,7 @@ export function getRoom({ return LivechatTyped.getRoom(guest, message, roomInfo, agent, extraParams); } -export async function findAgent(agentId: string): Promise { +export async function findAgent(agentId?: string): Promise { return normalizeAgent(agentId); } diff --git a/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts b/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts index af50ee5527e..648337a8c2a 100644 --- a/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts +++ b/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts @@ -12,7 +12,7 @@ callbacks.add( return message; } - if (!RoutingManager.getConfig().showQueue) { + if (!RoutingManager.getConfig()?.showQueue) { // since last message is only getting used on UI as preview message when queue is enabled return message; } diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.js b/apps/meteor/app/livechat/server/lib/RoutingManager.ts similarity index 71% rename from apps/meteor/app/livechat/server/lib/RoutingManager.js rename to apps/meteor/app/livechat/server/lib/RoutingManager.ts index 1419ac73868..aed24ad928d 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.js +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -2,6 +2,16 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { LivechatInquiry, LivechatRooms, Subscriptions, Rooms, Users } from '@rocket.chat/models'; import { Message } from '@rocket.chat/core-services'; +import type { + ILivechatInquiryRecord, + ILivechatVisitor, + IOmnichannelRoom, + IRoutingMethod, + IRoutingMethodConstructor, + RoutingMethodConfig, + SelectedAgent, + InquiryWithAgentInfo, +} from '@rocket.chat/core-typings'; import { createLivechatSubscription, @@ -19,7 +29,45 @@ import { Apps, AppEvents } from '../../../../ee/server/apps'; const logger = new Logger('RoutingManager'); -export const RoutingManager = { +type Routing = { + methodName: string | null; + methods: Record; + startQueue(): void; + isMethodSet(): boolean; + setMethodNameAndStartQueue(name: string): void; + registerMethod(name: string, Method: IRoutingMethodConstructor): void; + getMethod(): IRoutingMethod; + getConfig(): RoutingMethodConfig | undefined; + getNextAgent(department?: string, ignoreAgentId?: string): Promise; + delegateInquiry( + inquiry: InquiryWithAgentInfo, + agent?: SelectedAgent | null, + options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId: string; transferData: any } }, + ): Promise; + assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent): Promise; + unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string): Promise; + takeInquiry( + inquiry: Omit< + ILivechatInquiryRecord, + 'estimatedInactivityCloseTimeAt' | 'message' | 't' | 'source' | 'estimatedWaitingTimeQueue' | 'priorityWeight' | '_updatedAt' + >, + agent: SelectedAgent | null, + options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId: string; transferData: any } }, + ): Promise; + transferRoom( + room: IOmnichannelRoom, + guest: ILivechatVisitor, + transferData: { + departmentId?: string; + userId?: string; + transferredBy: { _id: string }; + }, + ): Promise; + delegateAgent(agent: SelectedAgent, inquiry: ILivechatInquiryRecord): Promise; + removeAllRoomSubscriptions(room: Pick, ignoreUser?: { _id: string }): Promise; +}; + +export const RoutingManager: Routing = { methodName: null, methods: {}, @@ -44,12 +92,16 @@ export const RoutingManager = { this.startQueue(); }, + // eslint-disable-next-line @typescript-eslint/naming-convention registerMethod(name, Method) { logger.debug(`Registering new routing method with name ${name}`); this.methods[name] = new Method(); }, getMethod() { + if (!this.methodName) { + throw new Meteor.Error('error-routing-method-not-set'); + } if (!this.methods[this.methodName]) { throw new Meteor.Error('error-routing-method-not-available'); } @@ -57,7 +109,7 @@ export const RoutingManager = { }, getConfig() { - return this.getMethod().config || {}; + return this.getMethod().config; }, async getNextAgent(department, ignoreAgentId) { @@ -71,7 +123,7 @@ export const RoutingManager = { 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}`); agent = await this.getNextAgent(department); - logger.debug(`Routing method returned agent ${agent && agent.agentId} for inquiry ${inquiry._id}`); + logger.debug(`Routing method returned agent ${agent?.agentId} for inquiry ${inquiry._id}`); } if (!agent) { @@ -101,17 +153,19 @@ export const RoutingManager = { } await LivechatRooms.changeAgentByRoomId(rid, agent); - await Rooms.incUsersCountById(rid); + await Rooms.incUsersCountById(rid, 1); const user = await Users.findOneById(agent.agentId); const room = await LivechatRooms.findOneById(rid); - await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]); + if (user) { + await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]); + } await dispatchAgentDelegated(rid, agent.agentId); logger.debug(`Agent ${agent.agentId} assigned to inquriy ${inquiry._id}. Instances notified`); - Apps.getBridges().getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); + void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); return inquiry; }, @@ -120,7 +174,7 @@ export const RoutingManager = { const room = await LivechatRooms.findOneById(rid); logger.debug(`Removing assignations of inquiry ${inquiry._id}`); - if (!room || !room.open) { + if (!room?.open) { logger.debug(`Cannot unassign agent from inquiry ${inquiry._id}: Room already closed`); return false; } @@ -171,7 +225,7 @@ export const RoutingManager = { const { _id, rid } = inquiry; const room = await LivechatRooms.findOneById(rid); - if (!room || !room.open) { + if (!room?.open) { logger.debug(`Cannot take Inquiry ${inquiry._id}: Room is closed`); return room; } @@ -196,12 +250,16 @@ export const RoutingManager = { if (!agent) { logger.debug(`Cannot take Inquiry ${inquiry._id}: Precondition failed for agent`); - const cbRoom = await callbacks.run('livechat.onAgentAssignmentFailed', { inquiry, room, options }); + const cbRoom = await callbacks.run<'livechat.onAgentAssignmentFailed'>('livechat.onAgentAssignmentFailed', { + inquiry, + room, + options, + }); return cbRoom; } await LivechatInquiry.takeInquiry(_id); - const inq = await this.assignAgent(inquiry, agent); + const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, agent); logger.debug(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); callbacks.runAsync('livechat.afterTakeInquiry', inq, agent); @@ -249,7 +307,8 @@ export const RoutingManager = { if (ignoreUser && ignoreUser._id === u._id) { return; } - removeAgentFromSubscription(roomId, u); + // @ts-expect-error - File still in JS, expecting error for now on `u` types + void removeAgentFromSubscription(roomId, u); }); }, }; diff --git a/apps/meteor/app/livechat/server/methods/getRoutingConfig.ts b/apps/meteor/app/livechat/server/methods/getRoutingConfig.ts index da54882f588..364853a7451 100644 --- a/apps/meteor/app/livechat/server/methods/getRoutingConfig.ts +++ b/apps/meteor/app/livechat/server/methods/getRoutingConfig.ts @@ -7,7 +7,7 @@ import { RoutingManager } from '../lib/RoutingManager'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'livechat:getRoutingConfig'(): OmichannelRoutingConfig; + 'livechat:getRoutingConfig'(): OmichannelRoutingConfig | undefined; } } diff --git a/apps/meteor/app/livechat/server/methods/takeInquiry.ts b/apps/meteor/app/livechat/server/methods/takeInquiry.ts index dcc400c1a9d..1bf84347b0f 100644 --- a/apps/meteor/app/livechat/server/methods/takeInquiry.ts +++ b/apps/meteor/app/livechat/server/methods/takeInquiry.ts @@ -9,14 +9,17 @@ import { settings } from '../../../settings/server'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'livechat:takeInquiry'(inquiryId: string, options?: { clientAction: boolean; forwardingToDepartment?: boolean }): unknown; + 'livechat:takeInquiry'( + inquiryId: string, + options?: { clientAction: boolean; forwardingToDepartment?: { oldDepartmentId: string; transferData: any } }, + ): unknown; } } export const takeInquiry = async ( userId: string, inquiryId: string, - options?: { clientAction: boolean; forwardingToDepartment?: boolean }, + options?: { clientAction: boolean; forwardingToDepartment?: { oldDepartmentId: string; transferData: any } }, ): Promise => { if (!userId || !(await hasPermissionAsync(userId, 'view-l-room'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { diff --git a/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts b/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts index 931451ad120..0bad086e738 100644 --- a/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts +++ b/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts @@ -41,8 +41,7 @@ export const validators: OmnichannelRoomAccessValidator[] = [ if (!user?._id) { return false; } - const { previewRoom } = RoutingManager.getConfig(); - if (!previewRoom) { + if (!RoutingManager.getConfig()?.previewRoom) { return; } 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 60fdc144c95..cf473477178 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts @@ -40,7 +40,7 @@ settings.watch('Livechat_last_chatted_agent_routing', function (value) return inquiry; } - if (!RoutingManager.getConfig().autoAssignAgent) { + if (!RoutingManager.getConfig()?.autoAssignAgent) { return inquiry; } @@ -64,7 +64,7 @@ settings.watch('Livechat_last_chatted_agent_routing', function (value) return inquiry; } - if (!RoutingManager.getConfig().autoAssignAgent) { + if (!RoutingManager.getConfig()?.autoAssignAgent) { return inquiry; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index 8434fcce07f..d73c2e4d594 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -89,7 +89,7 @@ class AutoTransferChatSchedulerClass { const timeoutDuration = settings.get('Livechat_auto_transfer_chat_timeout').toString(); - if (!RoutingManager.getConfig().autoAssignAgent) { + if (!RoutingManager.getConfig()?.autoAssignAgent) { this.logger.debug(`Auto-assign agent is disabled, returning room ${roomId} as inquiry`); await Livechat.returnRoomAsInquiry(room._id, departmentId, { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts index 44a88a0da32..d6a6c8f07d3 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts @@ -32,6 +32,7 @@ type QueueInfo = { statistics: Document; numberMostRecentChats: number; }; + export const getMaxNumberSimultaneousChat = async ({ agentId, departmentId }: { agentId?: string; departmentId?: string }) => { if (departmentId) { const department = await LivechatDepartmentRaw.findOneById(departmentId); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts index 15dd04e335e..995ac4f8cab 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts @@ -378,7 +378,7 @@ function shouldQueueStart() { return; } - const routingSupportsAutoAssign = RoutingManager.getConfig().autoAssignAgent; + const routingSupportsAutoAssign = RoutingManager.getConfig()?.autoAssignAgent; queueLogger.debug( `Routing method ${RoutingManager.methodName} supports auto assignment: ${routingSupportsAutoAssign}. ${ routingSupportsAutoAssign ? 'Starting' : 'Stopping' diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 8a8f1bb4384..32a4e59a7b4 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -18,6 +18,7 @@ import type { IOmnichannelRoom, ILivechatTag, SelectedAgent, + InquiryWithAgentInfo, } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; @@ -60,7 +61,7 @@ interface EventLikeCallbackSignatures { 'livechat:afterReturnRoomAsInquiry': (params: { room: IRoom }) => void; 'livechat.setUserStatusLivechat': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; 'livechat.agentStatusChanged': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; - 'livechat.afterTakeInquiry': (inq: ILivechatInquiryRecord, agent: { agentId: string; username: string }) => void; + 'livechat.afterTakeInquiry': (inq: InquiryWithAgentInfo, agent: { agentId: string; username: string }) => void; 'afterAddedToRoom': (params: { user: IUser; inviter?: IUser }, room: IRoom) => void; 'beforeAddedToRoom': (params: { user: IUser; inviter: IUser }) => void; 'afterCreateDirectRoom': (params: IRoom, second: { members: IUser[]; creatorId: IUser['_id'] }) => void; @@ -156,19 +157,7 @@ type ChainedCallbackSignatures = { agentsId: ILivechatAgent['_id'][]; }; 'livechat.applySimultaneousChatRestrictions': (_: undefined, params: { departmentId?: ILivechatDepartmentRecord['_id'] }) => undefined; - 'livechat.beforeDelegateAgent': ( - agent: { - agentId: string; - username: string; - }, - params?: { department?: string }, - ) => - | { - agentId: string; - username: string; - } - | null - | undefined; + 'livechat.beforeDelegateAgent': (agent: SelectedAgent | undefined, params?: { department?: string }) => SelectedAgent | null | undefined; 'livechat.applyDepartmentRestrictions': ( query: FilterOperators, params: { userId: IUser['_id'] }, diff --git a/packages/core-typings/src/IInquiry.ts b/packages/core-typings/src/IInquiry.ts index 056d770002c..26ba2bc3218 100644 --- a/packages/core-typings/src/IInquiry.ts +++ b/packages/core-typings/src/IInquiry.ts @@ -1,7 +1,7 @@ import type { ILivechatPriority } from './ILivechatPriority'; import type { IOmnichannelRoom, OmnichannelSourceType } from './IRoom'; import type { IOmnichannelServiceLevelAgreements } from './IOmnichannelServiceLevelAgreements'; -import type { IUser } from './IUser'; +import type { SelectedAgent } from './omnichannel/routing'; import type { IMessage } from './IMessage'; import type { IRocketChatRecord } from './IRocketChatRecord'; @@ -41,10 +41,7 @@ export interface ILivechatInquiryRecord extends IRocketChatRecord { locked?: boolean; lockedAt?: Date; lastMessage?: IMessage & { token?: string }; - defaultAgent?: { - agentId: IUser['_id']; - username?: IUser['username']; - }; + defaultAgent?: SelectedAgent; source: { type: OmnichannelSourceType; }; diff --git a/packages/core-typings/src/omnichannel/routing.ts b/packages/core-typings/src/omnichannel/routing.ts index e105b9e913c..bef2a7946f6 100644 --- a/packages/core-typings/src/omnichannel/routing.ts +++ b/packages/core-typings/src/omnichannel/routing.ts @@ -18,4 +18,5 @@ export type SelectedAgent = { }; export interface IRoutingMethod { getNextAgent(departmentId?: string, ignoreAgentId?: string): Promise; + config?: RoutingMethodConfig; }