Files
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

377 lines
11 KiB
TypeScript

import { api } from '@rocket.chat/core-services';
import type {
ILivechatDepartment,
IOmnichannelRoom,
IOmnichannelServiceLevelAgreements,
InquiryWithAgentInfo,
} from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import {
Rooms as RoomRaw,
LivechatRooms,
LivechatDepartment as LivechatDepartmentRaw,
LivechatCustomField,
LivechatInquiry,
Users,
} from '@rocket.chat/models';
import moment from 'moment';
import type { Document } from 'mongodb';
import { OmnichannelQueueInactivityMonitor } from './QueueInactivityMonitor';
import { updateInquiryQueueSla } from './SlaHelper';
import { memoizeDebounce } from './debounceByParams';
import { logger } from './logger';
import { getOmniChatSortQuery } from '../../../../app/livechat/lib/inquiries';
import { callbacks } from '../../../../server/lib/callbacks';
import { getInquirySortMechanismSetting } from '../../../../server/lib/omnichannel/settings';
import { settings } from '../../../../server/settings';
type QueueInfo = {
message: {
text: any;
user: {
_id: string;
username: string;
};
};
statistics: Document;
numberMostRecentChats: number;
};
export const isAgentWithinChatLimits = async ({
agentId,
departmentId,
totalChats,
departmentChats,
}: {
agentId: string;
departmentId?: string;
totalChats: number;
departmentChats: number;
}): Promise<boolean> => {
let agentLimit = 0;
let globalLimit = 0;
const user = await Users.getAgentInfo(agentId, settings.get('Livechat_show_agent_info'));
const rawAgentLimit = user?.livechat?.maxNumberSimultaneousChat;
if (rawAgentLimit !== undefined && rawAgentLimit !== null) {
const numericAgentLimit = Number(rawAgentLimit);
if (numericAgentLimit > 0) {
agentLimit = numericAgentLimit;
}
}
if (agentLimit === 0) {
const settingLimit = settings.get<number>('Livechat_maximum_chats_per_agent');
if (settingLimit > 0) {
globalLimit = settingLimit;
}
}
if (departmentId) {
const department = await LivechatDepartmentRaw.findOneById<Pick<ILivechatDepartment, 'maxNumberSimultaneousChat'>>(departmentId, {
projection: { maxNumberSimultaneousChat: 1 },
});
let departmentLimit = 0;
if (department?.maxNumberSimultaneousChat !== undefined && department.maxNumberSimultaneousChat !== null) {
const numericDeptLimit = Number(department.maxNumberSimultaneousChat);
if (numericDeptLimit > 0) {
departmentLimit = numericDeptLimit;
}
}
if (departmentLimit > 0) {
if (agentLimit > 0) {
logger.debug({
msg: 'Applying chat limits',
departmentId,
agentId,
limits: ['department', 'agent'],
totalChats,
departmentChats,
agentLimit,
departmentLimit,
});
return departmentChats < departmentLimit && totalChats < agentLimit;
}
if (globalLimit > 0) {
logger.debug({
msg: 'Applying chat limits',
departmentId,
agentId,
limits: ['department', 'global'],
totalChats,
departmentChats,
globalLimit,
departmentLimit,
});
return departmentChats < departmentLimit && totalChats < globalLimit;
}
logger.debug({
msg: 'Applying chat limits',
departmentId,
agentId,
limits: ['department'],
totalChats,
departmentChats,
departmentLimit,
});
return departmentChats < departmentLimit;
}
}
if (agentLimit > 0) {
logger.debug({
msg: 'Applying chat limits',
departmentId,
agentId,
limits: ['agent'],
totalChats,
agentLimit,
});
return totalChats < agentLimit;
}
if (globalLimit > 0) {
logger.debug({
msg: 'Applying chat limits',
departmentId,
agentId,
limits: ['global'],
totalChats,
globalLimit,
});
return totalChats < globalLimit;
}
logger.debug({ msg: 'No applicable limit found for user', agentId });
return true;
};
const getWaitingQueueMessage = async (departmentId?: string) => {
const department = departmentId ? await LivechatDepartmentRaw.findOneById(departmentId) : null;
if (department?.waitingQueueMessage) {
return department.waitingQueueMessage;
}
return settings.get<string>('Livechat_waiting_queue_message');
};
const getQueueInfo = async (department?: string) => {
const numberMostRecentChats = settings.get<number>('Livechat_number_most_recent_chats_estimate_wait_time');
const statistics = await RoomRaw.getMostRecentAverageChatDurationTime(numberMostRecentChats, department);
const text = await getWaitingQueueMessage(department);
const message = {
text,
user: { _id: 'rocket.cat', username: 'rocket.cat' },
};
return { message, statistics, numberMostRecentChats };
};
const getSpotEstimatedWaitTime = (spot: number, maxNumberSimultaneousChat: number, avgChatDuration: number) => {
if (!maxNumberSimultaneousChat || !avgChatDuration) {
return;
}
// X = spot
// N = maxNumberSimultaneousChat
// Estimated Wait Time = ([(N-1)/X]+1) *Average Chat Time of Most Recent X(Default = 100) Chats
return ((spot - 1) / maxNumberSimultaneousChat + 1) * avgChatDuration;
};
const normalizeQueueInfo = async ({
position,
queueInfo,
department,
}: {
position: number;
department?: string;
queueInfo?: QueueInfo;
}) => {
if (!queueInfo) {
queueInfo = await getQueueInfo(department);
}
const { message, numberMostRecentChats, statistics: { avgChatDuration } = {} } = queueInfo;
const spot = position + 1;
const estimatedWaitTimeSeconds = getSpotEstimatedWaitTime(spot, numberMostRecentChats, avgChatDuration);
return { spot, message, estimatedWaitTimeSeconds };
};
export const dispatchInquiryPosition = async (inquiry: Omit<InquiryWithAgentInfo, 'v'>, queueInfo?: QueueInfo) => {
const { position, department } = inquiry;
// Avoid broadcasting if no position was determined
if (position === undefined) {
return;
}
const data = await normalizeQueueInfo({ position, queueInfo, department });
return setTimeout(() => {
void api.broadcast('omnichannel.room', inquiry.rid, {
type: 'queueData',
data,
});
}, 1000);
};
const dispatchWaitingQueueStatus = async (department?: string) => {
if (!settings.get('Livechat_waiting_queue') && !settings.get('Omnichannel_calculate_dispatch_service_queue_statistics')) {
return;
}
const queue = await LivechatInquiry.getCurrentSortedQueueAsync({
department,
queueSortBy: getOmniChatSortQuery(getInquirySortMechanismSetting()),
});
if (!queue.length) {
return;
}
const queueInfo = await getQueueInfo(department);
queue.forEach((inquiry) => {
void dispatchInquiryPosition(inquiry, queueInfo);
});
};
// When dealing with lots of queued items we need to make sure to notify their position
// but we don't need to notify _each_ change that takes place, just their final position
export const debouncedDispatchWaitingQueueStatus = memoizeDebounce(dispatchWaitingQueueStatus, 1200);
export const setPredictedVisitorAbandonmentTime = async (
room: Pick<IOmnichannelRoom, '_id' | 'responseBy' | 'departmentId'>,
roomUpdater?: Updater<IOmnichannelRoom>,
) => {
if (
!room.responseBy?.firstResponseTs ||
!settings.get('Livechat_abandoned_rooms_action') ||
settings.get('Livechat_abandoned_rooms_action') === 'none'
) {
return;
}
let secondsToAdd = settings.get<number>('Livechat_visitor_inactivity_timeout');
const department = room.departmentId ? await LivechatDepartmentRaw.findOneById(room.departmentId) : null;
if (department?.visitorInactivityTimeoutInSeconds) {
secondsToAdd = department.visitorInactivityTimeoutInSeconds;
}
if (secondsToAdd <= 0) {
return;
}
const willBeAbandonedAt = moment(room.responseBy.firstResponseTs).add(Number(secondsToAdd), 'seconds').toDate();
if (roomUpdater) {
await LivechatRooms.getPredictedVisitorAbandonmentByRoomIdUpdateQuery(willBeAbandonedAt, roomUpdater);
} else {
await LivechatRooms.setPredictedVisitorAbandonmentByRoomId(room._id, willBeAbandonedAt);
}
};
export const updatePredictedVisitorAbandonment = async () => {
if (!settings.get('Livechat_abandoned_rooms_action') || settings.get('Livechat_abandoned_rooms_action') === 'none') {
await LivechatRooms.unsetAllPredictedVisitorAbandonment();
} else {
// Eng day: use a promise queue to update the predicted visitor abandonment time instead of all at once
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const promisesArray: Promise<void>[] = [];
await LivechatRooms.findOpen(extraQuery).forEach((room) => {
promisesArray.push(setPredictedVisitorAbandonmentTime(room));
});
await Promise.all(promisesArray);
}
};
export const updateQueueInactivityTimeout = async () => {
const queueTimeout = settings.get<number>('Livechat_max_queue_wait_time');
if (queueTimeout <= 0) {
await OmnichannelQueueInactivityMonitor.stop();
return;
}
await LivechatInquiry.getQueuedInquiries({ projection: { _updatedAt: 1 } }).forEach((inq) => {
const aggregatedDate = moment(inq._updatedAt).add(queueTimeout, 'minutes');
try {
void OmnichannelQueueInactivityMonitor.scheduleInquiry(inq._id, new Date(aggregatedDate.format()));
} catch (e) {
// this will usually happen if other instance attempts to re-create a job
logger.error({ err: e });
}
});
};
export const updateSLAInquiries = async (
executedBy: string,
sla?: Pick<IOmnichannelServiceLevelAgreements, '_id' | 'dueTimeInMinutes'>,
) => {
if (!sla) {
return;
}
const { _id: slaId } = sla;
const promises: Promise<void>[] = [];
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: executedBy });
await LivechatRooms.findOpenBySlaId(slaId, {}, extraQuery).forEach((room) => {
promises.push(updateInquiryQueueSla(room._id, sla));
});
await Promise.allSettled(promises);
};
export const getLivechatCustomFields = async () => {
const customFields = await LivechatCustomField.find({
visibility: 'visible',
scope: 'visitor',
public: true,
}).toArray();
return customFields.map(({ _id, label, regexp, required = false, type, defaultValue = null, options }) => ({
_id,
label,
regexp,
required,
type,
defaultValue,
...(options && options !== '' && { options: options.split(',') }),
}));
};
export const getLivechatQueueInfo = async (room?: IOmnichannelRoom) => {
if (!room) {
return null;
}
if (!settings.get('Livechat_waiting_queue')) {
return null;
}
if (!settings.get('Omnichannel_calculate_dispatch_service_queue_statistics')) {
return null;
}
const { _id: rid, departmentId: department } = room;
const inquiry = await LivechatInquiry.findOneByRoomId(rid, { projection: { _id: 1, status: 1 } });
if (!inquiry) {
return null;
}
const { _id, status } = inquiry;
if (status !== 'queued') {
return null;
}
const [inq] = await LivechatInquiry.getCurrentSortedQueueAsync({
inquiryId: _id,
department,
queueSortBy: getOmniChatSortQuery(getInquirySortMechanismSetting()),
});
if (!inq) {
return null;
}
return normalizeQueueInfo(inq);
};