snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatRooms, LivechatDepartment } from '@rocket.chat/models';
import { onNewRoom } from '../../../../server/lib/omnichannel/hooks';
onNewRoom.patch(async (originalFn, room) => {
await originalFn(room);
if (!room.departmentId) {
return;
}
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'ancestors'>>(room.departmentId, {
projection: { ancestors: 1 },
});
if (!department?.ancestors) {
return;
}
const { ancestors } = department;
await LivechatRooms.updateDepartmentAncestorsById(room._id, ancestors);
});

View File

@@ -0,0 +1,19 @@
import { LivechatRooms } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
callbacks.add(
'livechat.afterForwardChatToAgent',
async (options: { rid?: string } = {}) => {
const { rid } = options;
if (!rid) {
return options;
}
await LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(rid);
return options;
},
callbacks.priority.MEDIUM,
'livechat-after-forward-room-to-department',
);

View File

@@ -0,0 +1,44 @@
import type { ILivechatDepartment, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms, LivechatDepartment } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
import { cbLogger } from '../../lib/omnichannel/logger';
callbacks.add(
'livechat.afterForwardChatToDepartment',
async (options) => {
const { rid, newDepartmentId } = options;
const room = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, '_id' | 'departmentAncestors'>>(rid, {
projection: { departmentAncestors: 1 },
});
if (!room) {
return options;
}
await LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id);
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'ancestors'>>(newDepartmentId, {
projection: { ancestors: 1 },
});
if (!department) {
return options;
}
const { departmentAncestors } = room;
const { ancestors } = department;
if (!ancestors && !departmentAncestors) {
return options;
}
cbLogger.debug({
msg: 'Updating department ancestors for room',
departmentId: newDepartmentId,
roomId: rid,
});
await LivechatRooms.updateDepartmentAncestorsById(room._id, ancestors);
return options;
},
callbacks.priority.MEDIUM,
'livechat-after-forward-room-to-department',
);

View File

@@ -0,0 +1,22 @@
import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings';
import moment from 'moment';
import { afterInquiryQueued } from '../../../../server/lib/omnichannel/hooks';
import { settings } from '../../../../server/settings';
import { OmnichannelQueueInactivityMonitor } from '../../lib/omnichannel/QueueInactivityMonitor';
export const afterInquiryQueuedFunc = async (inquiry: ILivechatInquiryRecord) => {
const timer = settings.get<number>('Livechat_max_queue_wait_time');
if (!inquiry?._id || !inquiry?._updatedAt || timer <= 0) {
return void 0;
}
// schedule individual jobs instead of property for close inactivty
const newQueueTime = moment(inquiry._updatedAt).add(timer, 'minutes');
await OmnichannelQueueInactivityMonitor.scheduleInquiry(inquiry._id, new Date(newQueueTime.format()));
};
afterInquiryQueued.patch(async (originalFn: any, inquiry: ILivechatInquiryRecord) => {
await originalFn();
return afterInquiryQueuedFunc(inquiry);
});

View File

@@ -0,0 +1,36 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { callbacks } from '../../../../server/lib/callbacks';
import { i18n } from '../../../../server/lib/i18n';
import { settings } from '../../../../server/settings';
import { AutoCloseOnHoldScheduler } from '../../lib/omnichannel/AutoCloseOnHoldScheduler';
import { cbLogger } from '../../lib/omnichannel/logger';
let autoCloseOnHoldChatTimeout = 0;
const handleAfterOnHold = async (room: Pick<IOmnichannelRoom, '_id'>): Promise<any> => {
const { _id: rid } = room;
if (!rid) {
return;
}
if (!autoCloseOnHoldChatTimeout || autoCloseOnHoldChatTimeout <= 0) {
return;
}
cbLogger.debug({ msg: 'Scheduling room to be closed', rid, autoCloseOnHoldChatTimeout });
const closeComment =
settings.get<string>('Livechat_auto_close_on_hold_chats_custom_message') ||
i18n.t('Closed_automatically_because_chat_was_onhold_for_seconds', {
onHoldTime: autoCloseOnHoldChatTimeout,
});
await AutoCloseOnHoldScheduler.scheduleRoom(rid, autoCloseOnHoldChatTimeout, closeComment);
};
settings.watch<number>('Livechat_auto_close_on_hold_chats_timeout', (value) => {
autoCloseOnHoldChatTimeout = value as number;
if (!value || value <= 0) {
callbacks.remove('livechat:afterOnHold', 'livechat-auto-close-on-hold');
}
callbacks.add('livechat:afterOnHold', handleAfterOnHold, callbacks.priority.HIGH, 'livechat-auto-close-on-hold');
});

View File

@@ -0,0 +1,27 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { callbacks } from '../../../../server/lib/callbacks';
import { AutoCloseOnHoldScheduler } from '../../lib/omnichannel/AutoCloseOnHoldScheduler';
import { cbLogger } from '../../lib/omnichannel/logger';
type IRoom = Pick<IOmnichannelRoom, '_id'>;
const handleAfterOnHoldChatResumed = async (room: IRoom): Promise<IRoom> => {
if (!room?._id) {
return room;
}
const { _id: roomId } = room;
cbLogger.debug({ msg: 'Removing current on hold timers for room', roomId });
await AutoCloseOnHoldScheduler.unscheduleRoom(roomId);
return room;
};
callbacks.add(
'livechat:afterOnHoldChatResumed',
handleAfterOnHoldChatResumed,
callbacks.priority.HIGH,
'livechat-after-on-hold-chat-resumed',
);

View File

@@ -0,0 +1,35 @@
import type { AtLeast, ILivechatAgent, ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatUnit } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
import { cbLogger } from '../../lib/omnichannel/logger';
const afterRemoveDepartment = async (options: {
department: AtLeast<ILivechatDepartment, '_id' | 'businessHourId' | 'parentId'>;
agentsId: ILivechatAgent['_id'][];
}) => {
if (!options?.department) {
cbLogger.warn({ msg: 'No department found in options', options });
return options;
}
const { department } = options;
cbLogger.debug({
msg: 'Post removal actions on EE code for department',
department,
});
await Promise.all([
LivechatDepartment.removeDepartmentFromForwardListById(department._id),
...(department.parentId ? [LivechatUnit.decrementDepartmentsCount(department.parentId)] : []),
]);
return options;
};
callbacks.add(
'livechat.afterRemoveDepartment',
(options) => afterRemoveDepartment(options),
callbacks.priority.HIGH,
'livechat-after-remove-department',
);

View File

@@ -0,0 +1,24 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
import { settings } from '../../../../server/settings';
settings.watch('Livechat_abandoned_rooms_action', (value) => {
if (!value || value === 'none') {
callbacks.remove('livechat:afterReturnRoomAsInquiry', 'livechat-after-return-room-as-inquiry');
return;
}
callbacks.add(
'livechat:afterReturnRoomAsInquiry',
({ room }: { room: IOmnichannelRoom }) => {
if (!room?._id || !room?.omnichannel?.predictedVisitorAbandonmentAt) {
return;
}
return LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id);
},
callbacks.priority.HIGH,
'livechat-after-return-room-as-inquiry',
);
});

View File

@@ -0,0 +1,14 @@
import { CannedResponse } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
callbacks.add(
'livechat.afterTagRemoved',
async (tag) => {
const { name } = tag;
await CannedResponse.removeTagFromCannedResponses(name);
},
callbacks.priority.MEDIUM,
'on-tag-removed-remove-references',
);

View File

@@ -0,0 +1,54 @@
import type { InquiryWithAgentInfo, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatVisitors } from '@rocket.chat/models';
import { RoutingManager } from '../../../../server/lib/omnichannel/RoutingManager';
import { afterTakeInquiry } from '../../../../server/lib/omnichannel/hooks';
import { settings } from '../../../../server/settings';
import { AutoTransferChatScheduler } from '../../lib/omnichannel/AutoTransferChatScheduler';
import { debouncedDispatchWaitingQueueStatus } from '../../lib/omnichannel/Helper';
import { OmnichannelQueueInactivityMonitor } from '../../lib/omnichannel/QueueInactivityMonitor';
import { cbLogger } from '../../lib/omnichannel/logger';
afterTakeInquiry.patch(
async (
originalFn,
{ inquiry, room, agent }: { inquiry: InquiryWithAgentInfo; room: IOmnichannelRoom; agent: { agentId: string; username: string } },
) => {
await originalFn({ inquiry, room, agent });
if (!inquiry) {
return;
}
if (settings.get('Livechat_waiting_queue')) {
const { department } = inquiry;
void debouncedDispatchWaitingQueueStatus(department);
cbLogger.debug({
msg: 'Queue status updated',
queue: department || 'public',
});
}
if (settings.get<boolean>('Livechat_last_chatted_agent_routing') && agent && RoutingManager.getConfig()?.autoAssignAgent) {
const { v: { token } = {} } = inquiry;
if (token) {
await LivechatVisitors.updateLastAgentByToken(token, { ...agent, ts: new Date() });
cbLogger.info({
msg: 'Updated last agent of visitor',
token,
newAgent: { _id: agent.agentId, username: agent.username },
});
}
}
const autoTransferTimeout = settings.get<number>('Livechat_auto_transfer_chat_timeout');
if (autoTransferTimeout && !room?.autoTransferredAt && !room?.autoTransferOngoing) {
await AutoTransferChatScheduler.scheduleRoom(inquiry.rid, autoTransferTimeout);
}
const maxQueueWaitTime = settings.get<number>('Livechat_max_queue_wait_time');
if (maxQueueWaitTime > 0) {
await OmnichannelQueueInactivityMonitor.stopInquiry(inquiry._id);
}
},
);

View File

@@ -0,0 +1,23 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import type { FilterOperators } from 'mongodb';
import { callbacks } from '../../../../server/lib/callbacks';
import { restrictQuery } from '../../lib/omnichannel/restrictQuery';
callbacks.add(
'livechat.applyRoomRestrictions',
async (
originalQuery: FilterOperators<IOmnichannelRoom> = {},
{
unitsFilter,
userId,
}: {
unitsFilter?: string[];
userId?: string;
} = {},
) => {
return restrictQuery({ originalQuery, unitsFilter, userId });
},
callbacks.priority.HIGH,
'livechat-apply-room-restrictions',
);

View File

@@ -0,0 +1,44 @@
import type { ILivechatDepartment, AvailableAgentsAggregation } from '@rocket.chat/core-typings';
import { LivechatDepartment } from '@rocket.chat/models';
import type { Filter } from 'mongodb';
import { callbacks } from '../../../../server/lib/callbacks';
import { settings } from '../../../../server/settings';
export async function getChatLimitsQuery(departmentId?: string): Promise<Filter<AvailableAgentsAggregation>> {
const limitFilter: Filter<AvailableAgentsAggregation> = [];
if (departmentId) {
const departmentLimit =
(
await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'maxNumberSimultaneousChat'>>(departmentId, {
projection: { maxNumberSimultaneousChat: 1 },
})
)?.maxNumberSimultaneousChat || 0;
if (departmentLimit > 0) {
limitFilter.push({ 'queueInfo.chatsForDepartment': { $gte: Number(departmentLimit) } });
}
}
limitFilter.push({
$and: [{ maxChatsForAgent: { $gt: 0 } }, { $expr: { $gte: ['$queueInfo.chats', '$maxChatsForAgent'] } }],
});
const maxChatsPerSetting = settings.get<number>('Livechat_maximum_chats_per_agent');
if (maxChatsPerSetting > 0) {
limitFilter.push({
$and: [{ maxChatsForAgent: { $eq: 0 } }, { 'queueInfo.chats': { $gte: maxChatsPerSetting } }],
});
}
return { $match: { $or: limitFilter } };
}
callbacks.add(
'livechat.applySimultaneousChatRestrictions',
async (_: any, { departmentId }: { departmentId?: string } = {}) => {
return getChatLimitsQuery(departmentId);
},
callbacks.priority.HIGH,
'livechat-apply-simultaneous-restrictions',
);

View File

@@ -0,0 +1,36 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../../server/lib/callbacks';
callbacks.add(
'livechat.beforeForwardRoomToDepartment',
async (options) => {
const { room, transferData } = options;
if (!room || !transferData) {
return options;
}
const { departmentId } = room;
if (!departmentId) {
return options;
}
const { department: departmentToTransfer } = transferData;
const currentDepartment = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'departmentsAllowedToForward'>>(departmentId, {
projection: { departmentsAllowedToForward: 1 },
});
if (!currentDepartment) {
return options;
}
const { departmentsAllowedToForward } = currentDepartment;
const isAllowedToTransfer =
!departmentsAllowedToForward?.length ||
(Array.isArray(departmentsAllowedToForward) && departmentsAllowedToForward.includes(departmentToTransfer._id));
if (isAllowedToTransfer) {
return options;
}
throw new Meteor.Error('error-forwarding-department-target-not-allowed', 'The forwarding to the target department is not allowed.');
},
callbacks.priority.HIGH,
'livechat-before-forward-room-to-department',
);

View File

@@ -0,0 +1,34 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import type { IUser, IRoom } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
import { settings } from '../../../../server/settings';
import { isAgentWithinChatLimits } from '../../lib/omnichannel/Helper';
callbacks.add(
'beforeJoinRoom',
async (user: IUser, room?: IRoom): Promise<IUser> => {
if (!settings.get('Livechat_waiting_queue')) {
return user;
}
if (!room || !isOmnichannelRoom(room)) {
return user;
}
const { departmentId } = room;
const userSubs = await Users.getAgentAndAmountOngoingChats(user._id, departmentId);
if (!userSubs) {
return user;
}
const { queueInfo: { chats = 0, chatsForDepartment = 0 } = {} } = userSubs;
if (await isAgentWithinChatLimits({ agentId: user._id, departmentId, totalChats: chats, departmentChats: chatsForDepartment })) {
return user;
}
throw new Error('error-max-number-simultaneous-chats-reached');
},
callbacks.priority.MEDIUM,
'livechat-before-join-room',
);

View File

@@ -0,0 +1,10 @@
import { LivechatTag } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
callbacks.add(
'livechat.beforeListTags',
() => LivechatTag.find({}, { projection: { name: 1, departments: 1 } }).toArray(),
callbacks.priority.LOW,
'livechat-before-list-tags',
);

View File

@@ -0,0 +1,52 @@
import type { ILivechatInquiryRecord, ILivechatPriority, IOmnichannelServiceLevelAgreements } from '@rocket.chat/core-typings';
import { LivechatPriority, OmnichannelServiceLevelAgreements } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../../server/lib/callbacks';
callbacks.add(
'livechat.beforeInquiry',
async (extraData) => {
const { sla: slaSearchTerm, priority: prioritySearchTerm, ...props } = extraData;
if (!slaSearchTerm && !prioritySearchTerm) {
return extraData;
}
let sla: IOmnichannelServiceLevelAgreements | null = null;
let priority: ILivechatPriority | null = null;
if (slaSearchTerm) {
sla = await OmnichannelServiceLevelAgreements.findOneByIdOrName(slaSearchTerm, {
projection: { dueTimeInMinutes: 1 },
});
if (!sla) {
throw new Meteor.Error('error-invalid-sla', 'Invalid sla', {
function: 'livechat.beforeInquiry',
});
}
}
if (prioritySearchTerm) {
priority = await LivechatPriority.findOneByIdOrName(prioritySearchTerm, {
projection: { _id: 1, sortItem: 1 },
});
if (!priority) {
throw new Meteor.Error('error-invalid-priority', 'Invalid priority', {
function: 'livechat.beforeInquiry',
});
}
}
const ts = new Date();
const changes: Partial<ILivechatInquiryRecord> = {
ts,
};
if (sla) {
changes.slaId = sla._id;
changes.estimatedWaitingTimeQueue = sla.dueTimeInMinutes;
}
if (priority) {
changes.priorityId = priority._id;
changes.priorityWeight = priority.sortItem;
}
return { ...props, ...changes };
},
callbacks.priority.MEDIUM,
'livechat-before-new-inquiry',
);

View File

@@ -0,0 +1,35 @@
import type { IOmnichannelRoomInfo, IOmnichannelRoomExtraData, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { OmnichannelServiceLevelAgreements } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { isPlainObject } from '../../../../lib/utils/isPlainObject';
import { beforeNewRoom } from '../../../../server/lib/omnichannel/hooks';
export const beforeNewRoomPatched = async (
_next: any,
roomInfo: IOmnichannelRoomInfo,
extraData?: IOmnichannelRoomExtraData,
): Promise<Partial<IOmnichannelRoom>> => {
if (!extraData) {
return roomInfo;
}
const { sla: searchTerm, customFields } = extraData;
const roomInfoWithExtraData = { ...roomInfo, ...(isPlainObject(customFields) && { customFields }) };
if (!searchTerm) {
return roomInfoWithExtraData;
}
const sla = await OmnichannelServiceLevelAgreements.findOneByIdOrName(searchTerm);
if (!sla) {
throw new Meteor.Error('error-invalid-sla', 'Invalid sla', {
function: 'livechat.beforeRoom',
});
}
const { _id: slaId } = sla;
return { ...roomInfoWithExtraData, slaId };
};
beforeNewRoom.patch(beforeNewRoomPatched);

View File

@@ -0,0 +1,73 @@
import type { ILivechatInquiryRecord, SelectedAgent, ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatInquiry, LivechatRooms } from '@rocket.chat/models';
import { notifyOnLivechatInquiryChanged, notifyOnRoomChangedById } from '../../../../server/lib/notifyListener';
import { allowAgentSkipQueue } from '../../../../server/lib/omnichannel/Helper';
import { saveQueueInquiry } from '../../../../server/lib/omnichannel/QueueManager';
import { setDepartmentForGuest } from '../../../../server/lib/omnichannel/departmentsLib';
import { beforeRouteChat } from '../../../../server/lib/omnichannel/hooks';
import { online } from '../../../../server/lib/omnichannel/service-status';
import { settings } from '../../../../server/settings';
import { cbLogger } from '../../lib/omnichannel/logger';
beforeRouteChat.patch(
async (
originalFn: (inquiry: ILivechatInquiryRecord, _agent?: SelectedAgent | null) => Promise<ILivechatInquiryRecord | null | undefined>,
inquiry: ILivechatInquiryRecord,
agent?: SelectedAgent | null,
) => {
await originalFn(inquiry, agent);
if (!inquiry) {
return inquiry;
}
if (inquiry?.department && !(await online(inquiry.department, true, true))) {
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'fallbackForwardDepartment'>>(
inquiry.department,
{
projection: { fallbackForwardDepartment: 1 },
},
);
if (!department) {
return inquiry;
}
if (department.fallbackForwardDepartment) {
cbLogger.info({
msg: 'Moving inquiry to fallback department',
originalDepartment: inquiry.department,
fallbackDepartment: department.fallbackForwardDepartment,
inquiryId: inquiry._id,
});
const [, updatedLivechatInquiry] = await Promise.all([
setDepartmentForGuest({
visitorId: inquiry.v._id,
department: department.fallbackForwardDepartment,
}),
LivechatInquiry.setDepartmentByInquiryId(inquiry._id, department.fallbackForwardDepartment),
LivechatRooms.setDepartmentByRoomId(inquiry.rid, department.fallbackForwardDepartment),
]);
if (updatedLivechatInquiry) {
void notifyOnLivechatInquiryChanged(updatedLivechatInquiry, 'updated', { department: updatedLivechatInquiry.department });
}
void notifyOnRoomChangedById(inquiry.rid, 'updated');
inquiry = updatedLivechatInquiry ?? inquiry;
}
}
if (!settings.get('Livechat_waiting_queue')) {
return inquiry;
}
if (agent && (await allowAgentSkipQueue(agent))) {
return inquiry;
}
return saveQueueInquiry(inquiry);
},
);

View File

@@ -0,0 +1,65 @@
import { Users } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
import { allowAgentSkipQueue } from '../../../../server/lib/omnichannel/Helper';
import { checkOnlineAgents } from '../../../../server/lib/omnichannel/service-status';
import { settings } from '../../../../server/settings';
import { isAgentWithinChatLimits } from '../../lib/omnichannel/Helper';
import { cbLogger } from '../../lib/omnichannel/logger';
const validateMaxChats = async ({
agent,
inquiry,
}: {
agent: {
agentId: string;
};
inquiry: {
_id: string;
department: string;
};
options: {
forwardingToDepartment?: {
oldDepartmentId: string;
transferData: any;
};
clientAction?: boolean;
};
}) => {
if (!inquiry?._id || !agent?.agentId) {
cbLogger.debug('No inquiry or agent provided');
throw new Error('No inquiry or agent provided');
}
const { agentId } = agent;
if (!(await checkOnlineAgents(undefined, agent))) {
cbLogger.debug('Provided agent is not online');
throw new Error('Provided agent is not online');
}
if (!settings.get('Livechat_waiting_queue')) {
cbLogger.info({ msg: 'Chat can be taken by Agent: waiting queue is disabled', agentId });
return agent;
}
if (await allowAgentSkipQueue(agent)) {
cbLogger.info({ msg: 'Chat can be taken by Agent: agent can skip queue', agentId });
return agent;
}
const { department: departmentId } = inquiry;
const user = await Users.getAgentAndAmountOngoingChats(agentId, departmentId);
if (!user) {
cbLogger.debug({ msg: 'No valid agent found', agentId });
throw new Error('No valid agent found');
}
const { queueInfo: { chats = 0, chatsForDepartment = 0 } = {} } = user;
if (await isAgentWithinChatLimits({ agentId, departmentId, totalChats: chats, departmentChats: chatsForDepartment })) {
return user;
}
throw new Error('error-max-number-simultaneous-chats-reached');
};
callbacks.add('livechat.checkAgentBeforeTakeInquiry', validateMaxChats, callbacks.priority.MEDIUM, 'livechat-before-take-inquiry');

View File

@@ -0,0 +1,144 @@
import type { IUser, SelectedAgent } from '@rocket.chat/core-typings';
import { LivechatVisitors, LivechatContacts, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
import { notifyOnLivechatInquiryChanged } from '../../../../server/lib/notifyListener';
import { RoutingManager } from '../../../../server/lib/omnichannel/RoutingManager';
import { migrateVisitorIfMissingContact } from '../../../../server/lib/omnichannel/contacts/migrateVisitorIfMissingContact';
import { checkDefaultAgentOnNewRoom } from '../../../../server/lib/omnichannel/hooks';
import { settings } from '../../../../server/settings';
const normalizeDefaultAgent = (agent?: Pick<IUser, '_id' | 'username'> | null): SelectedAgent | undefined => {
if (!agent) {
return undefined;
}
const { _id: agentId, username } = agent;
return { agentId, username };
};
const getDefaultAgent = async ({ username, id }: { username?: string; id?: string }): Promise<SelectedAgent | undefined> => {
if (!username && !id) {
return undefined;
}
if (id) {
const agent = await Users.findOneOnlineAgentById(
id,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
{
projection: { _id: 1, username: 1 },
},
);
if (agent) {
return normalizeDefaultAgent(agent);
}
const offlineAgent = await Users.findOneAgentById(id, { projection: { _id: 1, username: 1 } });
if (offlineAgent && settings.get('Livechat_accept_chats_with_no_agents')) {
return normalizeDefaultAgent(offlineAgent);
}
return undefined;
}
return normalizeDefaultAgent(
await Users.findOneOnlineAgentByUserList(
username || [],
{ projection: { _id: 1, username: 1 } },
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
),
);
};
settings.watch<boolean>('Livechat_last_chatted_agent_routing', (value) => {
if (!value) {
callbacks.remove('livechat.onMaxNumberSimultaneousChatsReached', 'livechat-on-max-number-simultaneous-chats-reached');
return;
}
callbacks.add(
'livechat.onMaxNumberSimultaneousChatsReached',
async (inquiry) => {
if (!inquiry?.defaultAgent) {
return inquiry;
}
if (!RoutingManager.getConfig()?.autoAssignAgent) {
return inquiry;
}
await LivechatInquiry.removeDefaultAgentById(inquiry._id);
void notifyOnLivechatInquiryChanged(inquiry, 'updated', {
defaultAgent: undefined,
});
return LivechatInquiry.findOneById(inquiry._id);
},
callbacks.priority.MEDIUM,
'livechat-on-max-number-simultaneous-chats-reached',
);
});
checkDefaultAgentOnNewRoom.patch(async (_next, defaultAgent, { visitorId, source } = {}) => {
if (!visitorId || !source) {
return defaultAgent;
}
const guest = await LivechatVisitors.findOneEnabledById(visitorId, {
projection: { lastAgent: 1, token: 1, contactManager: 1 },
});
if (!guest) {
return defaultAgent;
}
const hasDivergentContactManager = defaultAgent?.agentId !== guest?.contactManager;
if (!hasDivergentContactManager && defaultAgent) {
return defaultAgent;
}
const contactId = await migrateVisitorIfMissingContact(visitorId, source);
const contact = contactId ? await LivechatContacts.findOneEnabledById(contactId, { projection: { contactManager: 1 } }) : undefined;
const contactManagerPreferred = settings.get<boolean>('Omnichannel_contact_manager_routing');
const guestManager = contactManagerPreferred && (await getDefaultAgent({ id: contact?.contactManager }));
if (guestManager) {
return guestManager;
}
if (!settings.get<boolean>('Livechat_last_chatted_agent_routing')) {
return defaultAgent;
}
const { lastAgent, token } = guest;
const guestAgent = await getDefaultAgent({ username: lastAgent?.username });
if (guestAgent) {
return guestAgent;
}
const room = await LivechatRooms.findOneLastServedAndClosedByVisitorToken(token, {
projection: { servedBy: 1 },
});
if (!room?.servedBy) {
return defaultAgent;
}
const {
servedBy: { username: usernameByRoom },
} = room;
if (!usernameByRoom) {
return defaultAgent;
}
const lastRoomAgent = normalizeDefaultAgent(
await Users.findOneOnlineAgentByUserList(
usernameByRoom,
{ projection: { _id: 1, username: 1 } },
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
),
);
return lastRoomAgent;
});

View File

@@ -0,0 +1,26 @@
import './addDepartmentAncestors';
import './afterForwardChatToDepartment';
import './beforeListTags';
import './beforeJoinRoom';
import './setPredictedVisitorAbandonmentTime';
import './beforeForwardRoomToDepartment';
import './afterRemoveDepartment';
import './onLoadForwardDepartmentRestrictions';
import './afterTakeInquiry';
import './beforeNewInquiry';
import './beforeNewRoom';
import './beforeRoutingChat';
import './checkAgentBeforeTakeInquiry';
import './onCloseLivechat';
import './onSaveVisitorInfo';
import './onBusinessHourStart';
import './onAgentAssignmentFailed';
import './afterOnHoldChatResumed';
import './afterReturnRoomAsInquiry';
import './afterForwardChatToAgent';
import './applySimultaneousChatsRestrictions';
import './afterInquiryQueued';
import './sendPdfTranscriptOnClose';
import './applyRoomRestrictions';
import './afterTagRemoved';
import './manageDepartmentUnit';

View File

@@ -0,0 +1,59 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatUnit } from '@rocket.chat/models';
import { getUnitsFromUser } from '@rocket.chat/omni-core-ee';
import { hasAnyRoleAsync } from '../../../../server/lib/authorization/hasRole';
import { callbacks } from '../../../../server/lib/callbacks';
export const manageDepartmentUnit = async ({ userId, departmentId, unitId }: { userId: string; departmentId: string; unitId: string }) => {
const accessibleUnits = await getUnitsFromUser(userId);
const isLivechatManager = await hasAnyRoleAsync(userId, ['admin', 'livechat-manager']);
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'ancestors' | 'parentId'>>(departmentId, {
projection: { ancestors: 1, parentId: 1 },
});
const isDepartmentAlreadyInUnit = unitId && department?.ancestors?.includes(unitId);
if (!department || isDepartmentAlreadyInUnit) {
return;
}
const currentDepartmentUnitId = department.parentId;
const canManageNewUnit = !unitId || isLivechatManager || (Array.isArray(accessibleUnits) && accessibleUnits.includes(unitId));
const canManageCurrentUnit =
!currentDepartmentUnitId || isLivechatManager || (Array.isArray(accessibleUnits) && accessibleUnits.includes(currentDepartmentUnitId));
if (!canManageNewUnit || !canManageCurrentUnit) {
return;
}
if (unitId) {
const unit = await LivechatUnit.findOneById(
unitId,
{
projection: { ancestors: 1 },
},
{
unitsFromUser: accessibleUnits,
},
);
if (!unit) {
return;
}
if (currentDepartmentUnitId) {
await LivechatUnit.decrementDepartmentsCount(currentDepartmentUnitId);
}
await LivechatDepartment.addDepartmentToUnit(departmentId, unitId, [unitId, ...(unit.ancestors || [])]);
await LivechatUnit.incrementDepartmentsCount(unitId);
return;
}
if (currentDepartmentUnitId) {
await LivechatUnit.decrementDepartmentsCount(currentDepartmentUnitId);
}
await LivechatDepartment.removeDepartmentFromUnit(departmentId);
};
callbacks.add('livechat.manageDepartmentUnit', manageDepartmentUnit, callbacks.priority.HIGH, 'livechat-manage-department-unit');

View File

@@ -0,0 +1,46 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { callbacks } from '../../../../server/lib/callbacks';
import { settings } from '../../../../server/settings';
const handleOnAgentAssignmentFailed = async (
room: IOmnichannelRoom,
{
inquiry,
options,
}: {
inquiry: any;
options: {
forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any };
clientAction?: boolean;
};
},
) => {
if (!inquiry || !room) {
return false;
}
if (!settings.get('Livechat_waiting_queue')) {
return false;
}
const { forwardingToDepartment: { oldDepartmentId } = {}, forwardingToDepartment } = options;
if (!forwardingToDepartment) {
return false;
}
const { department: newDepartmentId } = inquiry;
if (!newDepartmentId || !oldDepartmentId || newDepartmentId === oldDepartmentId) {
return false;
}
return { ...room, chatQueued: true } as IOmnichannelRoom & { chatQueued: boolean };
};
callbacks.add(
'livechat.onAgentAssignmentFailed',
handleOnAgentAssignmentFailed,
callbacks.priority.HIGH,
'livechat-agent-assignment-failed',
);

View File

@@ -0,0 +1,21 @@
import { LivechatBusinessHourBehaviors } from '@rocket.chat/core-typings';
import { callbacks } from '../../../../server/lib/callbacks';
import { settings } from '../../../../server/settings';
import { MultipleBusinessHoursBehavior } from '../../lib/omnichannel/business-hour/Multiple';
callbacks.add(
'on-business-hour-start',
(options: any = {}) => {
const { BusinessHourBehaviorClass } = options;
if (!BusinessHourBehaviorClass) {
return options;
}
if (settings.get('Livechat_business_hour_type') === LivechatBusinessHourBehaviors.SINGLE) {
return options;
}
return { BusinessHourBehaviorClass: MultipleBusinessHoursBehavior };
},
callbacks.priority.HIGH,
'livechat-on-business-hour-start',
);

View File

@@ -0,0 +1,5 @@
import { Match } from 'meteor/check';
import { onCheckRoomParams } from '../../../../server/api/v1/omnichannel/lib/livechat';
onCheckRoomParams.patch((_: any, params) => ({ ...params, sla: Match.Maybe(String), priority: Match.Maybe(String) }));

View File

@@ -0,0 +1,39 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
import { settings } from '../../../../server/settings';
import { AutoCloseOnHoldScheduler } from '../../lib/omnichannel/AutoCloseOnHoldScheduler';
import { debouncedDispatchWaitingQueueStatus } from '../../lib/omnichannel/Helper';
type LivechatCloseCallbackParams = {
room: IOmnichannelRoom;
};
const onCloseLivechat = async (params: LivechatCloseCallbackParams) => {
const {
room,
room: { _id: roomId },
} = params;
await LivechatRooms.unsetOnHoldByRoomId(roomId);
if (settings.get('Livechat_auto_close_on_hold_chats_timeout')) {
await AutoCloseOnHoldScheduler.unscheduleRoom(roomId);
}
if (!settings.get('Livechat_waiting_queue')) {
return params;
}
const { departmentId } = room || {};
debouncedDispatchWaitingQueueStatus(departmentId);
return params;
};
callbacks.add(
'livechat.closeRoom',
(params: LivechatCloseCallbackParams) => onCloseLivechat(params),
callbacks.priority.HIGH,
'livechat-on-close-livechat-remove-on-hold-and-dispatch-waiting-queue',
);

View File

@@ -0,0 +1,34 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { getExtraConfigInfo } from '../../../../server/api/v1/omnichannel/lib/livechat';
import { getLivechatQueueInfo, getLivechatCustomFields } from '../../lib/omnichannel/Helper';
getExtraConfigInfo.patch(
async (
_: any,
options: {
room?: IOmnichannelRoom;
},
): Promise<{
queueInfo?: unknown;
customFields?: {
options?: string[] | undefined;
_id: string;
label: string;
regexp: string | undefined;
required: boolean;
type: string | undefined;
defaultValue: string | null;
}[];
room?: IOmnichannelRoom;
}> => {
const { room } = options;
const [queueInfo, customFields] = await Promise.all([getLivechatQueueInfo(room), getLivechatCustomFields()]);
return {
...(queueInfo && { queueInfo }),
...(customFields && { customFields }),
...options,
};
},
);

View File

@@ -0,0 +1,27 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
callbacks.add(
'livechat.onLoadForwardDepartmentRestrictions',
async (options) => {
const { departmentId } = options;
if (!departmentId) {
return options;
}
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'departmentsAllowedToForward'>>(departmentId, {
projection: { departmentsAllowedToForward: 1 },
});
if (!department) {
return options;
}
const { departmentsAllowedToForward } = department;
if (!departmentsAllowedToForward) {
return options;
}
return Object.assign({ restrictions: { _id: { $in: departmentsAllowedToForward } } }, options);
},
callbacks.priority.MEDIUM,
'livechat-on-load-forward-department-restrictions',
);

View File

@@ -0,0 +1,53 @@
import type { IOmnichannelRoom, IOmnichannelServiceLevelAgreements, IUser } from '@rocket.chat/core-typings';
import { OmnichannelServiceLevelAgreements } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
import { removePriorityFromRoom, updateRoomPriority } from '../../api/v1/omnichannel/lib/priorities';
import { removeRoomSLA, updateRoomSLA } from '../../api/v1/omnichannel/lib/sla';
const updateSLA = async (room: IOmnichannelRoom, user: Required<Pick<IUser, '_id' | 'username' | 'name'>>, slaId?: string) => {
if (!slaId) {
return removeRoomSLA(room._id, user);
}
const sla: Pick<IOmnichannelServiceLevelAgreements, '_id' | 'name' | 'dueTimeInMinutes'> | null =
await OmnichannelServiceLevelAgreements.findOneById(slaId, {
projection: { _id: 1, name: 1, dueTimeInMinutes: 1 },
});
if (!sla) {
throw new Error(`SLA not found with id: ${slaId}`);
}
await updateRoomSLA(room._id, user, sla);
};
const updatePriority = async (room: IOmnichannelRoom, user: Required<Pick<IUser, '_id' | 'username' | 'name'>>, priorityId?: string) => {
if (!priorityId) {
return removePriorityFromRoom(room._id, user);
}
await updateRoomPriority(room._id, user, priorityId);
};
callbacks.add(
'livechat.saveInfo',
async (room, { user, oldRoom }: any) => {
const { slaId: oldSlaId, priorityId: oldPriorityId } = oldRoom;
const { slaId: newSlaId, priorityId: newPriorityId } = room;
if (oldSlaId === newSlaId && oldPriorityId === newPriorityId) {
return room;
}
if (oldSlaId === newSlaId && oldPriorityId !== newPriorityId) {
await updatePriority(room, user, newPriorityId);
} else if (oldSlaId !== newSlaId && oldPriorityId === newPriorityId) {
await updateSLA(room, user, newSlaId);
} else {
await Promise.all([updateSLA(room, user, newSlaId), updatePriority(room, user, newPriorityId)]);
}
return room as any;
},
callbacks.priority.HIGH,
'livechat-on-save-room-info',
);

View File

@@ -0,0 +1,85 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import type { IRoom, ILivechatVisitor, ILivechatDepartment, TransferData, AtLeast } from '@rocket.chat/core-typings';
import { LivechatDepartment } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
import { forwardRoomToDepartment } from '../../../../server/lib/omnichannel/Helper';
import { settings } from '../../../../server/settings';
import { cbLogger } from '../../lib/omnichannel/logger';
const onTransferFailure = async (
room: IRoom,
{
guest,
transferData,
department,
}: {
guest: ILivechatVisitor;
transferData: TransferData;
department: AtLeast<ILivechatDepartment, '_id' | 'fallbackForwardDepartment' | 'name'>;
},
) => {
if (!isOmnichannelRoom(room)) {
return false;
}
if (!department?.fallbackForwardDepartment?.length) {
return false;
}
// TODO: find enabled not archived here
const fallbackDepartment = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'name'>>(
department.fallbackForwardDepartment,
{
projection: { name: 1, _id: 1 },
},
);
if (!fallbackDepartment) {
return false;
}
const { hops: currentHops = 1 } = transferData;
const maxHops = settings.get<number>('Omnichannel_max_fallback_forward_depth');
if (currentHops > maxHops) {
cbLogger.debug({
msg: 'Max fallback forward depth reached. Chat wont be transfered',
roomId: room._id,
hops: currentHops,
maxHops,
departmentId: department._id,
});
return false;
}
const transferDataFallback = {
...transferData,
usingFallbackDep: true,
prevDepartment: department.name,
departmentId: department.fallbackForwardDepartment,
department: fallbackDepartment,
hops: currentHops + 1,
};
const forwardSuccess = await forwardRoomToDepartment(room, guest, transferDataFallback);
if (!forwardSuccess) {
cbLogger.debug({
msg: 'Fallback forward failed',
departmentId: department._id,
roomId: room._id,
fallbackDepartmentId: department.fallbackForwardDepartment,
});
return false;
}
cbLogger.info({
msg: 'Fallback forward success',
departmentId: department._id,
roomId: room._id,
fallbackDepartmentId: department.fallbackForwardDepartment,
});
return true;
};
callbacks.add('livechat:onTransferFailure', onTransferFailure, callbacks.priority.HIGH);

View File

@@ -0,0 +1,71 @@
import { OmnichannelEEService } from '@rocket.chat/core-services';
import type { ILivechatVisitor, IMessage, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
import { isMessageFromVisitor, isEditedMessage } from '@rocket.chat/core-typings';
import { LivechatRooms, LivechatVisitors, Users } from '@rocket.chat/models';
import { callbacks } from '../../../../server/lib/callbacks';
import { i18n } from '../../../../server/lib/i18n';
import { callbackLogger } from '../../../../server/lib/omnichannel/logger';
const resumeOnHoldCommentAndUser = async (room: IOmnichannelRoom): Promise<{ comment: string; resumedBy: IUser }> => {
const {
v: { _id: visitorId },
_id: rid,
} = room;
const visitor = await LivechatVisitors.findOneEnabledById<Pick<ILivechatVisitor, 'name' | 'username'>>(visitorId, {
projection: { name: 1, username: 1 },
});
if (!visitor) {
callbackLogger.error({ msg: '[afterOmnichannelSaveMessage] Visitor Not found for room while trying to resume on hold', rid });
throw new Error('Visitor not found while trying to resume on hold');
}
const guest = visitor.name || visitor.username;
const resumeChatComment = i18n.t('Omnichannel_on_hold_chat_automatically', { guest });
const resumedBy = await Users.findOneById('rocket.cat');
if (!resumedBy) {
callbackLogger.error({ msg: '[afterOmnichannelSaveMessage] User Not found for room while trying to resume on hold', rid });
throw new Error(`User not found while trying to resume on hold`);
}
return { comment: resumeChatComment, resumedBy };
};
callbacks.add(
'afterOmnichannelSaveMessage',
async (message: IMessage, { room }) => {
if (isEditedMessage(message) || message.t) {
return message;
}
const { _id: rid, v: roomVisitor } = room;
if (!roomVisitor?._id) {
return message;
}
// Need to read the room every time, the room object is not updated
const updatedRoom = await LivechatRooms.findOneById(rid);
if (!updatedRoom) {
return message;
}
if (isMessageFromVisitor(message) && room.onHold) {
callbackLogger.debug({ msg: '[afterOmnichannelSaveMessage] Room is on hold, resuming it now since visitor sent a message', rid });
try {
const { comment: resumeChatComment, resumedBy } = await resumeOnHoldCommentAndUser(updatedRoom);
await OmnichannelEEService.resumeRoomOnHold(updatedRoom, resumeChatComment, resumedBy);
} catch (error) {
callbackLogger.error({ msg: '[afterOmnichannelSaveMessage] Error while resuming room on hold', rid, err: error });
return message;
}
}
return message;
},
callbacks.priority.HIGH,
'livechat-resume-on-hold',
);

View File

@@ -0,0 +1,79 @@
import type { IMessage, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { callbacks } from '../../../../server/lib/callbacks';
import type { CloseRoomParams } from '../../../../server/lib/omnichannel/localTypes';
import { settings } from '../../../../server/settings';
import { AutoTransferChatScheduler } from '../../lib/omnichannel/AutoTransferChatScheduler';
type LivechatCloseCallbackParams = {
room: IOmnichannelRoom;
options: CloseRoomParams['options'];
};
let autoTransferTimeout = 0;
const handleAfterCloseRoom = async (params: LivechatCloseCallbackParams): Promise<LivechatCloseCallbackParams> => {
const { room } = params;
const { _id: rid, autoTransferredAt, autoTransferOngoing } = room;
if (!autoTransferTimeout || autoTransferTimeout <= 0) {
return params;
}
if (autoTransferredAt) {
return params;
}
if (!autoTransferOngoing) {
return params;
}
await AutoTransferChatScheduler.unscheduleRoom(rid);
return params;
};
settings.watch('Livechat_auto_transfer_chat_timeout', (value) => {
autoTransferTimeout = value as number;
if (!autoTransferTimeout || autoTransferTimeout === 0) {
callbacks.remove('afterOmnichannelSaveMessage', 'livechat-cancel-auto-transfer-job-after-message');
callbacks.remove('livechat.closeRoom', 'livechat-cancel-auto-transfer-on-close-room');
return;
}
callbacks.add(
'afterOmnichannelSaveMessage',
async (message: IMessage, { room }): Promise<IMessage> => {
const { _id: rid, autoTransferredAt, autoTransferOngoing } = room;
const { token, t: messageType } = message;
if (messageType) {
// ignore system messages
return message;
}
if (!autoTransferTimeout || autoTransferTimeout <= 0) {
return message;
}
if (!message || token) {
// ignore messages from visitors
return message;
}
if (autoTransferredAt) {
return message;
}
if (!autoTransferOngoing) {
return message;
}
await AutoTransferChatScheduler.unscheduleRoom(rid);
return message;
},
callbacks.priority.HIGH,
'livechat-cancel-auto-transfer-job-after-message',
);
callbacks.add('livechat.closeRoom', handleAfterCloseRoom, callbacks.priority.HIGH, 'livechat-cancel-auto-transfer-on-close-room');
});

View File

@@ -0,0 +1,32 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { callbacks } from '../../../../server/lib/callbacks';
import type { CloseRoomParams } from '../../../../server/lib/omnichannel/localTypes';
import { requestPdfTranscript } from '../../lib/omnichannel/requestPdfTranscript';
type LivechatCloseCallbackParams = {
room: IOmnichannelRoom;
options: CloseRoomParams['options'];
};
const sendPdfTranscriptOnClose = async (params: LivechatCloseCallbackParams): Promise<LivechatCloseCallbackParams> => {
const { room, options } = params;
if (!isOmnichannelRoom(room)) {
return params;
}
const { pdfTranscript } = options || {};
if (!pdfTranscript) {
return params;
}
const { requestedBy } = pdfTranscript;
await requestPdfTranscript(room, requestedBy);
return params;
};
callbacks.add('livechat.closeRoom', sendPdfTranscriptOnClose, callbacks.priority.HIGH, 'livechat-send-pdf-transcript-on-close-room');

View File

@@ -0,0 +1,46 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings';
import moment from 'moment';
import { markRoomResponded } from '../../../../server/hooks/omnichannel/markRoomResponded';
import { callbacks } from '../../../../server/lib/callbacks';
import { settings } from '../../../../server/settings';
import { setPredictedVisitorAbandonmentTime } from '../../lib/omnichannel/Helper';
function shouldSaveInactivity(message: IMessage): boolean {
if (message.t || isEditedMessage(message) || isMessageFromVisitor(message)) {
return false;
}
const abandonedRoomsAction = settings.get('Livechat_abandoned_rooms_action');
const visitorInactivityTimeout = settings.get<number>('Livechat_visitor_inactivity_timeout');
if (!abandonedRoomsAction || abandonedRoomsAction === 'none' || visitorInactivityTimeout <= 0) {
return false;
}
return true;
}
callbacks.remove('afterOmnichannelSaveMessage', 'markRoomResponded');
callbacks.add(
'afterOmnichannelSaveMessage',
async (message, { room, roomUpdater }) => {
const responseBy = await markRoomResponded(message, room, roomUpdater);
if (!shouldSaveInactivity(message)) {
return message;
}
if (!responseBy) {
return;
}
if (moment(responseBy.firstResponseTs).isSame(moment(message.ts))) {
await setPredictedVisitorAbandonmentTime({ ...room, responseBy }, roomUpdater);
}
},
callbacks.priority.MEDIUM,
'save-visitor-inactivity',
);