The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Rocket.Chat/apps/meteor/app/livechat/server/lib/RoutingManager.ts

317 lines
10 KiB

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,
dispatchAgentDelegated,
dispatchInquiryQueued,
forwardRoomToAgent,
forwardRoomToDepartment,
removeAgentFromSubscription,
updateChatDepartment,
allowAgentSkipQueue,
} from './Helper';
import { callbacks } from '../../../../lib/callbacks';
import { Logger } from '../../../../server/lib/logger/Logger';
import { Apps, AppEvents } from '../../../../ee/server/apps';
const logger = new Logger('RoutingManager');
type Routing = {
methodName: string | null;
methods: Record<string, IRoutingMethod>;
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<SelectedAgent | null | undefined>;
delegateInquiry(
inquiry: InquiryWithAgentInfo,
agent?: SelectedAgent | null,
options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId: string; transferData: any } },
): Promise<IOmnichannelRoom | null | void>;
assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent): Promise<InquiryWithAgentInfo>;
unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string): Promise<boolean>;
takeInquiry(
inquiry: Omit<
ILivechatInquiryRecord,
'estimatedInactivityCloseTimeAt' | 'message' | 't' | 'source' | 'estimatedWaitingTimeQueue' | 'priorityWeight' | '_updatedAt'
>,
agent: SelectedAgent | null,
options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId: string; transferData: any } },
): Promise<IOmnichannelRoom | null | void>;
transferRoom(
room: IOmnichannelRoom,
guest: ILivechatVisitor,
transferData: {
departmentId?: string;
userId?: string;
transferredBy: { _id: string };
},
): Promise<boolean>;
delegateAgent(agent: SelectedAgent, inquiry: ILivechatInquiryRecord): Promise<SelectedAgent | null | undefined>;
removeAllRoomSubscriptions(room: Pick<IOmnichannelRoom, '_id'>, ignoreUser?: { _id: string }): Promise<void>;
};
export const RoutingManager: Routing = {
methodName: null,
methods: {},
startQueue() {
// todo: move to eventemitter or middleware
// queue shouldn't start on CE
},
isMethodSet() {
return !!this.methodName;
},
setMethodNameAndStartQueue(name) {
logger.debug(`Changing default routing method from ${this.methodName} to ${name}`);
if (!this.methods[name]) {
logger.warn(`Cannot change routing method to ${name}. Selected Routing method does not exists. Defaulting to Manual_Selection`);
this.methodName = 'Manual_Selection';
} else {
this.methodName = name;
}
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');
}
return this.methods[this.methodName];
},
getConfig() {
return this.getMethod().config;
},
async getNextAgent(department, ignoreAgentId) {
logger.debug(`Getting next available agent with method ${this.methodName}`);
return this.getMethod().getNextAgent(department, ignoreAgentId);
},
[Patch] [EE] Improve Forwarding Department behaviour with Waiting queue feature (#22077) * [EE] Fix Forwarding Department not working with Waiting queue feature (cherry picked from commit 2b5004031a0555638bbf2822deccab3ecae61dc3) * add waiting queue feature enabled check (cherry picked from commit d9fe3e38a579d3ba589f9b9e1d353d4ffcfc4596) * Refactor (cherry picked from commit 1b5c9737085fe8dbc21ede1d2b39eefab5585729) * Fix forwarding of agents not working (cherry picked from commit b4b3c3da9e5affc80a33ee6c70cc1445f2494c35) * Apply suggestions from code review Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> (cherry picked from commit 0c6f7ec3b73f998c45df24b7b992745e830404d7) * Handle transfer api response properly on client + refactor (cherry picked from commit d750c5949d2cf1d738dd21289e9396e9aacffa1a) * Avoid passing unnecessary params (cherry picked from commit 53eec95407fdbc67608b5c3887db3765c3abca72) * Remove throw error. (cherry picked from commit deccab97333eec553e84dff52bc7527804c9fd34) * Improve throw message logic. (cherry picked from commit 85b015db474068fc9b3bbfb96713bace939a5c5f) * Fix conflicts * Remove all subscription from a chat placed on-hold. (cherry picked from commit d49c8fe47802587441d40f57a8db8c8ac162cb71) * up fuselage version * add string-helpers package * [EE] Improve Forwarding Department behaviour with Waiting queue feature (#22043) * [EE] Fix Forwarding Department not working with Waiting queue feature * add waiting queue feature enabled check * Refactor * Fix forwarding of agents not working * Apply suggestions from code review Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> * Handle transfer api response properly on client + refactor * Avoid passing unnecessary params * Remove throw error. * Improve throw message logic. * Fix on-hold queue. Methods have been removed in another PR. * Remove all subscription from a chat placed on-hold. Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> Co-authored-by: Tiago Evangelista Pinto <tiago.evangelista@rocket.chat>
5 years ago
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}`);
agent = await this.getNextAgent(department);
logger.debug(`Routing method returned agent ${agent?.agentId} for inquiry ${inquiry._id}`);
}
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);
}
logger.debug(`Inquiry ${inquiry._id} will be taken by agent ${agent.agentId}`);
[Patch] [EE] Improve Forwarding Department behaviour with Waiting queue feature (#22077) * [EE] Fix Forwarding Department not working with Waiting queue feature (cherry picked from commit 2b5004031a0555638bbf2822deccab3ecae61dc3) * add waiting queue feature enabled check (cherry picked from commit d9fe3e38a579d3ba589f9b9e1d353d4ffcfc4596) * Refactor (cherry picked from commit 1b5c9737085fe8dbc21ede1d2b39eefab5585729) * Fix forwarding of agents not working (cherry picked from commit b4b3c3da9e5affc80a33ee6c70cc1445f2494c35) * Apply suggestions from code review Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> (cherry picked from commit 0c6f7ec3b73f998c45df24b7b992745e830404d7) * Handle transfer api response properly on client + refactor (cherry picked from commit d750c5949d2cf1d738dd21289e9396e9aacffa1a) * Avoid passing unnecessary params (cherry picked from commit 53eec95407fdbc67608b5c3887db3765c3abca72) * Remove throw error. (cherry picked from commit deccab97333eec553e84dff52bc7527804c9fd34) * Improve throw message logic. (cherry picked from commit 85b015db474068fc9b3bbfb96713bace939a5c5f) * Fix conflicts * Remove all subscription from a chat placed on-hold. (cherry picked from commit d49c8fe47802587441d40f57a8db8c8ac162cb71) * up fuselage version * add string-helpers package * [EE] Improve Forwarding Department behaviour with Waiting queue feature (#22043) * [EE] Fix Forwarding Department not working with Waiting queue feature * add waiting queue feature enabled check * Refactor * Fix forwarding of agents not working * Apply suggestions from code review Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> * Handle transfer api response properly on client + refactor * Avoid passing unnecessary params * Remove throw error. * Improve throw message logic. * Fix on-hold queue. Methods have been removed in another PR. * Remove all subscription from a chat placed on-hold. Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> Co-authored-by: Tiago Evangelista Pinto <tiago.evangelista@rocket.chat>
5 years ago
return this.takeInquiry(inquiry, agent, options);
},
async assignAgent(inquiry, agent) {
check(
agent,
Match.ObjectIncluding({
agentId: String,
username: String,
}),
);
logger.debug(`Assigning agent ${agent.agentId} to inquiry ${inquiry._id}`);
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);
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`);
void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user });
return inquiry;
},
async unassignAgent(inquiry, departmentId) {
const { rid, department } = inquiry;
const room = await LivechatRooms.findOneById(rid);
logger.debug(`Removing assignations of inquiry ${inquiry._id}`);
if (!room?.open) {
logger.debug(`Cannot unassign agent from inquiry ${inquiry._id}: Room already closed`);
return false;
}
if (departmentId && departmentId !== department) {
logger.debug(`Switching department for inquiry ${inquiry._id} [Current: ${department} | Next: ${departmentId}]`);
await updateChatDepartment({
rid,
newDepartmentId: departmentId,
oldDepartmentId: department,
});
// Fake the department to delegate the inquiry;
inquiry.department = departmentId;
}
const { servedBy } = room;
if (servedBy) {
logger.debug(`Unassigning current agent for inquiry ${inquiry._id}`);
await LivechatRooms.removeAgentByRoomId(rid);
await this.removeAllRoomSubscriptions(room);
await dispatchAgentDelegated(rid, null);
}
await dispatchInquiryQueued(inquiry);
return true;
},
[NEW][Enterprise] Omnichannel On-Hold Queue (#20945) * Add settings for on-hold feature * Add On-Hold Section within Sidebar * On hold room UI * OnHold - Automatic * Add Manual on-hold button UI * OnHold - manual - Start manual On-Hold timer after agent reply - Stop manual On-Hold timer on visitor message - Show manual On-Hold Option when timer expires * Handle manual On-Hold event * Add permissions to manual on-hold feature * [New] Auto-Close On hold chats * Routing chat when an on-hold get is resumed * Apply suggestions from code review * Apply suggestions from code review * Add migration * Add new endpoint - livechat/placeChatOnHold * Move Resume Button login within livechatReadOnly file * Remove timeout on Manual On-Hold feature - From now on, the On-Hold option will appear within visitor Info panel, provided the agent has sent the last message * Apply suggestions from code review * Move resume On-Hold chat logic inside Queue Manager * Apply suggestions from code review Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> * Apply suggestions from code review * Use takeInquiry() to resume On-Hold chats * Fix failing test case * Minor improvements. * [Regression] Omnichannel On Hold feature - handle following impacted events - Returning chat to the queue - Forwarding chats - Closing chats * Fix failing test cases * Revert "[Regression] Omnichannel On hold Queue" * Prevent on hold chat from being returned or forwarded (cherry picked from commit 2edcb8234224ebad2b479282da3f9be6a5626db6) * Move checks to Livechat methods * Add releaseOnHoldChat() method * Fix callback returning promise. Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com>
5 years ago
async takeInquiry(inquiry, agent, options = { clientAction: false }) {
check(
agent,
Match.ObjectIncluding({
agentId: String,
username: String,
}),
);
check(
inquiry,
Match.ObjectIncluding({
_id: String,
rid: String,
status: String,
}),
);
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;
}
if (room.servedBy && room.servedBy._id === agent.agentId) {
logger.debug(`Cannot take Inquiry ${inquiry._id}: Already taken by agent ${room.servedBy._id}`);
return room;
}
try {
await callbacks.run('livechat.checkAgentBeforeTakeInquiry', {
agent,
inquiry,
options,
});
} catch (e) {
if (options.clientAction && !options.forwardingToDepartment) {
throw e;
}
agent = null;
}
if (!agent) {
logger.debug(`Cannot take Inquiry ${inquiry._id}: Precondition failed for agent`);
const cbRoom = await callbacks.run<'livechat.onAgentAssignmentFailed'>('livechat.onAgentAssignmentFailed', {
inquiry,
room,
options,
});
return cbRoom;
}
await LivechatInquiry.takeInquiry(_id);
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);
return LivechatRooms.findOneById(rid);
},
async transferRoom(room, guest, transferData) {
logger.debug(`Transfering room ${room._id} by ${transferData.transferredBy._id}`);
if (transferData.departmentId) {
logger.debug(`Transfering room ${room._id} to department ${transferData.departmentId}`);
return forwardRoomToDepartment(room, guest, transferData);
}
if (transferData.userId) {
logger.debug(`Transfering room ${room._id} to user ${transferData.userId}`);
return forwardRoomToAgent(room, transferData);
}
logger.debug(`Unable to transfer room ${room._id}: No target provided`);
return false;
},
async delegateAgent(agent, inquiry) {
logger.debug(`Delegating Inquiry ${inquiry._id}`);
const defaultAgent = await callbacks.run('livechat.beforeDelegateAgent', agent, {
department: inquiry?.department,
});
if (defaultAgent) {
logger.debug(`Delegating Inquiry ${inquiry._id} to agent ${defaultAgent.username}`);
await LivechatInquiry.setDefaultAgentById(inquiry._id, defaultAgent);
}
logger.debug(`Queueing inquiry ${inquiry._id}`);
await dispatchInquiryQueued(inquiry, defaultAgent);
return defaultAgent;
},
async removeAllRoomSubscriptions(room, ignoreUser) {
const { _id: roomId } = room;
const subscriptions = await Subscriptions.findByRoomId(roomId).toArray();
subscriptions?.forEach(({ u }) => {
if (ignoreUser && ignoreUser._id === u._id) {
return;
}
// @ts-expect-error - File still in JS, expecting error for now on `u` types
void removeAgentFromSubscription(roomId, u);
});
},
};