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
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:
@@ -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);
|
||||
});
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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);
|
||||
});
|
||||
36
apps/meteor/ee/server/hooks/omnichannel/afterOnHold.ts
Normal file
36
apps/meteor/ee/server/hooks/omnichannel/afterOnHold.ts
Normal 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');
|
||||
});
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
14
apps/meteor/ee/server/hooks/omnichannel/afterTagRemoved.ts
Normal file
14
apps/meteor/ee/server/hooks/omnichannel/afterTagRemoved.ts
Normal 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',
|
||||
);
|
||||
54
apps/meteor/ee/server/hooks/omnichannel/afterTakeInquiry.ts
Normal file
54
apps/meteor/ee/server/hooks/omnichannel/afterTakeInquiry.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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',
|
||||
);
|
||||
34
apps/meteor/ee/server/hooks/omnichannel/beforeJoinRoom.ts
Normal file
34
apps/meteor/ee/server/hooks/omnichannel/beforeJoinRoom.ts
Normal 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',
|
||||
);
|
||||
10
apps/meteor/ee/server/hooks/omnichannel/beforeListTags.ts
Normal file
10
apps/meteor/ee/server/hooks/omnichannel/beforeListTags.ts
Normal 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',
|
||||
);
|
||||
52
apps/meteor/ee/server/hooks/omnichannel/beforeNewInquiry.ts
Normal file
52
apps/meteor/ee/server/hooks/omnichannel/beforeNewInquiry.ts
Normal 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',
|
||||
);
|
||||
35
apps/meteor/ee/server/hooks/omnichannel/beforeNewRoom.ts
Normal file
35
apps/meteor/ee/server/hooks/omnichannel/beforeNewRoom.ts
Normal 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);
|
||||
73
apps/meteor/ee/server/hooks/omnichannel/beforeRoutingChat.ts
Normal file
73
apps/meteor/ee/server/hooks/omnichannel/beforeRoutingChat.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
});
|
||||
26
apps/meteor/ee/server/hooks/omnichannel/index.ts
Normal file
26
apps/meteor/ee/server/hooks/omnichannel/index.ts
Normal 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';
|
||||
@@ -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');
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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) }));
|
||||
39
apps/meteor/ee/server/hooks/omnichannel/onCloseLivechat.ts
Normal file
39
apps/meteor/ee/server/hooks/omnichannel/onCloseLivechat.ts
Normal 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',
|
||||
);
|
||||
34
apps/meteor/ee/server/hooks/omnichannel/onLoadConfigApi.ts
Normal file
34
apps/meteor/ee/server/hooks/omnichannel/onLoadConfigApi.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -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',
|
||||
);
|
||||
53
apps/meteor/ee/server/hooks/omnichannel/onSaveVisitorInfo.ts
Normal file
53
apps/meteor/ee/server/hooks/omnichannel/onSaveVisitorInfo.ts
Normal 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',
|
||||
);
|
||||
85
apps/meteor/ee/server/hooks/omnichannel/onTransferFailure.ts
Normal file
85
apps/meteor/ee/server/hooks/omnichannel/onTransferFailure.ts
Normal 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);
|
||||
71
apps/meteor/ee/server/hooks/omnichannel/resumeOnHold.ts
Normal file
71
apps/meteor/ee/server/hooks/omnichannel/resumeOnHold.ts
Normal 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',
|
||||
);
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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');
|
||||
@@ -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',
|
||||
);
|
||||
Reference in New Issue
Block a user