Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: prevent multiple room finds during requestRoom method #32363

Draft
wants to merge 13 commits into
base: develop
Choose a base branch
from
10 changes: 8 additions & 2 deletions apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@ export const dispatchAgentDelegated = async (rid: string, agentId?: string) => {
});
};

/**
* @deprecated
*/

export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, agent?: SelectedAgent | null) => {
if (!inquiry?._id) {
return;
Expand All @@ -351,10 +355,12 @@ export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, age
return;
}

if (!agent || !(await allowAgentSkipQueue(agent))) {
await saveQueueInquiry(inquiry);
if (agent && (await allowAgentSkipQueue(agent))) {
return;
}

await saveQueueInquiry(inquiry);

// Alert only the online agents of the queued request
const onlineAgents = await LivechatTyped.getOnlineAgents(department, agent);
if (!onlineAgents) {
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ class LivechatClass {
return;
}

return Users.findByIds<ILivechatAgent>(agentIds);
return Users.findByIds<ILivechatAgent>([...new Set(agentIds)]);
}
return Users.findOnlineAgents();
}
Expand Down
246 changes: 216 additions & 30 deletions apps/meteor/app/livechat/server/lib/QueueManager.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import { Omnichannel } from '@rocket.chat/core-services';
import type { ILivechatInquiryRecord, ILivechatVisitor, IMessage, IOmnichannelRoom, SelectedAgent } from '@rocket.chat/core-typings';
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import {
LivechatInquiryStatus,
type ILivechatInquiryRecord,
type ILivechatVisitor,
type IMessage,
type IOmnichannelRoom,
type SelectedAgent,
} from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
import { LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { callbacks } from '../../../../lib/callbacks';
import { checkServiceStatus, createLivechatRoom, createLivechatInquiry } from './Helper';
import { sendNotification } from '../../../lib/server';
import { settings } from '../../../settings/server';
import { i18n } from '../../../utils/lib/i18n';
import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue } from './Helper';
import { Livechat } from './LivechatTyped';
import { RoutingManager } from './RoutingManager';

const logger = new Logger('QueueManager');
Expand All @@ -17,28 +29,38 @@ export const saveQueueInquiry = async (inquiry: ILivechatInquiryRecord) => {
await callbacks.run('livechat.afterInquiryQueued', inquiry);
};

/**
* @deprecated
*/
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}`);

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
// Minimizing disruption

if (!room) {
await saveQueueInquiry(inquiry);
return;
}
const dbInquiry = await LivechatInquiry.findOneById(inquiry._id);

if (!dbInquiry) {
throw new Error('inquiry-not-found');
return QueueManager.requeueInquiry(inquiry, room, defaultAgent);
};

const getDepartment = async (department: string): Promise<string | undefined> => {
if (!department) {
return;
}

if (await LivechatDepartmentAgents.checkOnlineForDepartment(department)) {
return department;
}

if (dbInquiry.status === 'ready') {
logger.debug(`Inquiry with id ${inquiry._id} is ready. Delegating to agent ${inquiryAgent?.username}`);
return RoutingManager.delegateInquiry(dbInquiry, inquiryAgent);
const departmentDocument = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'fallbackForwardDepartment'>>(
department,
{
projection: { fallbackForwardDepartment: 1 },
},
);

if (departmentDocument?.fallbackForwardDepartment) {
return getDepartment(departmentDocument.fallbackForwardDepartment);
}
};

Expand All @@ -56,8 +78,89 @@ type queueManager = {
unarchiveRoom: (archivedRoom?: IOmnichannelRoom) => Promise<IOmnichannelRoom>;
};

export const QueueManager: queueManager = {
async requestRoom({ guest, message, roomInfo, agent, extraData }) {
export const QueueManager = new (class implements queueManager {
async requeueInquiry(inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, defaultAgent?: SelectedAgent) {
if (!(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
// Minimizing disruption
await saveQueueInquiry(inquiry);
return;
}

const inquiryAgent = await RoutingManager.delegateAgent(defaultAgent, inquiry);
logger.debug(`Delegating inquiry with id ${inquiry._id} to agent ${defaultAgent?.username}`);
await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent);
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);
}
}

private fnQueueInquiryStatus: (typeof QueueManager)['getInquiryStatus'] | undefined;

public patchInquiryStatus(fn: (typeof QueueManager)['getInquiryStatus']) {
this.fnQueueInquiryStatus = fn;
}

async getInquiryStatus({ room, agent }: { room: IOmnichannelRoom; agent?: SelectedAgent }): Promise<LivechatInquiryStatus> {
if (this.fnQueueInquiryStatus) {
return this.fnQueueInquiryStatus({ room, agent });
}

if (!(await Omnichannel.isWithinMACLimit(room))) {
return LivechatInquiryStatus.QUEUED;
}

if (RoutingManager.getConfig()?.autoAssignAgent) {
return LivechatInquiryStatus.READY;
}

if (!agent || !(await allowAgentSkipQueue(agent))) {
return LivechatInquiryStatus.QUEUED;
}

return LivechatInquiryStatus.READY;
}

async queueInquiry(inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, defaultAgent?: SelectedAgent | null) {
await callbacks.run('livechat.new-beforeRouteChat', inquiry);

if (inquiry.status === 'ready') {
return RoutingManager.delegateInquiry(inquiry, defaultAgent);
}

await callbacks.run('livechat.afterInquiryQueued', inquiry);

void callbacks.run('livechat.chatQueued', room);

await this.dispatchInquiryQueued(inquiry, room, defaultAgent);
}

async requestRoom({
guest,
// rid = Random.id(),
message,
roomInfo,
agent,
extraData,
}: {
guest: ILivechatVisitor;
rid?: string;
message?: Pick<IMessage, 'rid' | 'msg'>;
roomInfo: {
source?: IOmnichannelRoom['source'];
[key: string]: unknown;
};
agent?: SelectedAgent;
extraData?: Record<string, unknown>;
}) {
logger.debug(`Requesting a room for guest ${guest._id}`);
check(
message,
Expand All @@ -77,8 +180,43 @@ export const QueueManager: queueManager = {
}),
);

if (!(await checkServiceStatus({ guest, agent }))) {
throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
const defaultAgent =
(await callbacks.run('livechat.beforeDelegateAgent', agent, {
department: guest.department,
})) || undefined;

const department = guest.department && (await getDepartment(guest.department));

/**
* we have 4 cases here
* 1. agent and no department
* 2. no agent and no department
* 3. no agent and department
* 4. agent and department informed
*
* in case 1, we check if the agent is online
* in case 2, we check if there is at least one online agent in the whole service
* in case 3, we check if there is at least one online agent in the department
*
* the case 4 is weird, but we are not throwing an error, just because the application works in some mysterious way
* we don't have explicitly defined what to do in this case so we just kept the old behavior
* it seems that agent has priority over department
* but some cases department is handled before agent
*
*/

if (!settings.get('Livechat_accept_chats_with_no_agents')) {
if (agent && !defaultAgent) {
throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
}

if (!defaultAgent && guest.department && !department) {
throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
}

if (!agent && !guest.department && !(await Livechat.checkOnlineAgents())) {
throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
}
}

const { rid } = message;
Expand All @@ -95,6 +233,7 @@ export const QueueManager: queueManager = {
await createLivechatInquiry({
rid,
name,
initialStatus: await this.getInquiryStatus({ room, agent: defaultAgent }),
guest,
message,
extraData: { ...extraData, source: roomInfo.source },
Expand All @@ -108,19 +247,16 @@ export const QueueManager: queueManager = {
void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomStarted, room);
await LivechatRooms.updateRoomCount();

await queueInquiry(inquiry, agent);
logger.debug(`Inquiry ${inquiry._id} queued`);

const newRoom = await LivechatRooms.findOneById(rid);
const newRoom = (await this.queueInquiry(inquiry, room, defaultAgent)) ?? (await LivechatRooms.findOneById(rid));
if (!newRoom) {
logger.error(`Room with id ${rid} not found`);
throw new Error('room-not-found');
}

return newRoom;
},
}

async unarchiveRoom(archivedRoom) {
async unarchiveRoom(archivedRoom?: IOmnichannelRoom) {
if (!archivedRoom) {
throw new Error('no-room-to-unarchive');
}
Expand Down Expand Up @@ -159,9 +295,59 @@ export const QueueManager: queueManager = {
throw new Error('inquiry-not-found');
}

await queueInquiry(inquiry, defaultAgent);
await this.requeueInquiry(inquiry, room, defaultAgent);
logger.debug(`Inquiry ${inquiry._id} queued`);

return room;
},
};
}

private dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, agent?: SelectedAgent | null) => {
logger.debug(`Notifying agents of new inquiry ${inquiry._id} queued`);

const { department, rid, v } = inquiry;
// Alert only the online agents of the queued request
const onlineAgents = await Livechat.getOnlineAgents(department, agent);

if (!onlineAgents) {
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) {
const { _id, active, emails, language, status, statusConnection, username } = agent;
await sendNotification({
// fake a subscription in order to make use of the function defined above
subscription: {
rid,
u: {
_id,
},
receiver: [
{
active,
emails,
language,
status,
statusConnection,
username,
},
],
name: '',
},
sender: v,
hasMentionToAll: true, // consider all agents to be in the room
hasReplyToThread: false,
disableAllMessageNotifications: false,
hasMentionToHere: false,
message: { _id: '', u: v, msg: '' },
// we should use server's language for this type of messages instead of user's
notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName }, language),
room: { ...room, name: i18n.t('New_chat_in_queue', {}, language) },
mentionIds: [],
});
}
};
})();
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { type ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatInquiry, LivechatRooms } from '@rocket.chat/models';

import { online } from '../../../../../app/livechat/server/api/lib/livechat';
Expand Down Expand Up @@ -72,9 +72,33 @@ callbacks.add(
await dispatchInquiryPosition(inq);
}
}

return LivechatInquiry.findOneById(_id);
},
callbacks.priority.HIGH,
'livechat-before-routing-chat',
);

settings.watch('Omnichannel_calculate_dispatch_service_queue_statistics', async (value) => {
if (!value) {
callbacks.remove('livechat.new-beforeRouteChat', 'livechat-before-routing-chat-queue-statistics');
}

callbacks.add(
'livechat.new-beforeRouteChat',
async (inquiry) => {
if (inquiry.status !== 'ready') {
return;
}

const [inq] = await LivechatInquiry.getCurrentSortedQueueAsync({
inquiryId: inquiry._id,
department: inquiry.department,
queueSortBy: getInquirySortMechanismSetting(),
});
if (inq) {
await dispatchInquiryPosition(inq);
}
},
callbacks.priority.HIGH,
'livechat-before-routing-chat-queue-statistics',
);
});