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:
310
apps/meteor/server/lib/omnichannel/departmentsLib.ts
Normal file
310
apps/meteor/server/lib/omnichannel/departmentsLib.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import { AppEvents, Apps } from '@rocket.chat/apps';
|
||||
import type { LivechatDepartmentDTO, ILivechatDepartment, ILivechatDepartmentAgents, ILivechatAgent } from '@rocket.chat/core-typings';
|
||||
import { LivechatDepartment, LivechatDepartmentAgents, LivechatVisitors, LivechatRooms, Users } from '@rocket.chat/models';
|
||||
import { isDepartmentCreationAvailable } from '@rocket.chat/omni-core';
|
||||
import { check, Match } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { updateDepartmentAgents } from './Helper';
|
||||
import { afterDepartmentArchived, afterDepartmentUnarchived } from './hooks';
|
||||
import { livechatLogger } from './logger';
|
||||
import { settings } from '../../settings';
|
||||
import { callbacks } from '../callbacks';
|
||||
import { notifyOnLivechatDepartmentAgentChangedByDepartmentId, notifyOnLivechatDepartmentAgentChanged } from '../notifyListener';
|
||||
/**
|
||||
* @param {string|null} _id - The department id
|
||||
* @param {Partial<import('@rocket.chat/core-typings').ILivechatDepartment>} departmentData
|
||||
* @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents
|
||||
* @param {{_id?: string}} [departmentUnit] - The department's unit id
|
||||
*/
|
||||
export async function saveDepartment(
|
||||
userId: string,
|
||||
_id: string | null,
|
||||
departmentData: LivechatDepartmentDTO,
|
||||
departmentAgents?: {
|
||||
upsert?: { agentId: string; count?: number; order?: number }[];
|
||||
remove?: { agentId: string; count?: number; order?: number }[];
|
||||
},
|
||||
departmentUnit?: { _id?: string },
|
||||
) {
|
||||
if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') {
|
||||
throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', {
|
||||
method: 'livechat:saveDepartment',
|
||||
});
|
||||
}
|
||||
|
||||
const department = _id
|
||||
? await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'archived' | 'enabled' | 'parentId'>>(_id, {
|
||||
projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 },
|
||||
})
|
||||
: null;
|
||||
|
||||
if (departmentUnit && !departmentUnit._id && department && department.parentId) {
|
||||
const isLastDepartmentInUnit = (await LivechatDepartment.countDepartmentsInUnit(department.parentId)) === 1;
|
||||
if (isLastDepartmentInUnit) {
|
||||
throw new Meteor.Error('error-unit-cant-be-empty', "The last department in a unit can't be removed", {
|
||||
method: 'livechat:saveDepartment',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!department && !(await isDepartmentCreationAvailable())) {
|
||||
throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', {
|
||||
method: 'livechat:saveDepartment',
|
||||
});
|
||||
}
|
||||
|
||||
if (department?.archived && departmentData.enabled) {
|
||||
throw new Meteor.Error('error-archived-department-cant-be-enabled', 'Archived departments cant be enabled', {
|
||||
method: 'livechat:saveDepartment',
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Use AJV or Zod for validation (or the lib we are using rn)
|
||||
const defaultValidations: Record<string, Match.Matcher<any> | BooleanConstructor | StringConstructor> = {
|
||||
enabled: Boolean,
|
||||
name: String,
|
||||
description: Match.Optional(String),
|
||||
showOnRegistration: Boolean,
|
||||
email: String,
|
||||
showOnOfflineForm: Boolean,
|
||||
requestTagBeforeClosingChat: Match.Optional(Boolean),
|
||||
chatClosingTags: Match.Optional([String]),
|
||||
fallbackForwardDepartment: Match.Optional(String),
|
||||
departmentsAllowedToForward: Match.Optional([String]),
|
||||
allowReceiveForwardOffline: Match.Optional(Boolean),
|
||||
};
|
||||
|
||||
// The Livechat Form department support addition/custom fields, so those fields need to be added before validating
|
||||
Object.keys(departmentData).forEach((field) => {
|
||||
if (!defaultValidations.hasOwnProperty(field)) {
|
||||
defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean);
|
||||
}
|
||||
});
|
||||
|
||||
check(departmentData, defaultValidations);
|
||||
check(
|
||||
departmentAgents,
|
||||
Match.Maybe({
|
||||
upsert: Match.Maybe(Array),
|
||||
remove: Match.Maybe(Array),
|
||||
}),
|
||||
);
|
||||
|
||||
const { requestTagBeforeClosingChat, chatClosingTags, fallbackForwardDepartment } = departmentData;
|
||||
if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) {
|
||||
throw new Meteor.Error(
|
||||
'error-validating-department-chat-closing-tags',
|
||||
'At least one closing tag is required when the department requires tag(s) on closing conversations.',
|
||||
{ method: 'livechat:saveDepartment' },
|
||||
);
|
||||
}
|
||||
|
||||
if (_id && !department) {
|
||||
throw new Meteor.Error('error-department-not-found', 'Department not found', {
|
||||
method: 'livechat:saveDepartment',
|
||||
});
|
||||
}
|
||||
|
||||
if (fallbackForwardDepartment === _id) {
|
||||
throw new Meteor.Error(
|
||||
'error-fallback-department-circular',
|
||||
'Cannot save department. Circular reference between fallback department and department',
|
||||
);
|
||||
}
|
||||
|
||||
if (fallbackForwardDepartment) {
|
||||
const fallbackDep = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id'>>(fallbackForwardDepartment, {
|
||||
projection: { _id: 1 },
|
||||
});
|
||||
if (!fallbackDep) {
|
||||
throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', {
|
||||
method: 'livechat:saveDepartment',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const departmentDB = await LivechatDepartment.createOrUpdateDepartment(_id, departmentData);
|
||||
if (departmentDB && departmentAgents) {
|
||||
await updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled);
|
||||
}
|
||||
|
||||
if (department?.enabled !== departmentData.enabled) {
|
||||
void notifyOnLivechatDepartmentAgentChangedByDepartmentId(departmentDB._id, department ? 'updated' : 'inserted');
|
||||
}
|
||||
|
||||
// Disable event
|
||||
if (department?.enabled && !departmentDB?.enabled) {
|
||||
await callbacks.run('livechat.afterDepartmentDisabled', departmentDB);
|
||||
void Apps.self?.triggerEvent(AppEvents.IPostLivechatDepartmentDisabled, { department: departmentDB });
|
||||
}
|
||||
|
||||
if (departmentUnit) {
|
||||
await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id });
|
||||
}
|
||||
|
||||
return departmentDB;
|
||||
}
|
||||
|
||||
export async function archiveDepartment(_id: string) {
|
||||
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'businessHourId'>>(_id, {
|
||||
projection: { _id: 1, businessHourId: 1 },
|
||||
});
|
||||
|
||||
if (!department) {
|
||||
throw new Error('department-not-found');
|
||||
}
|
||||
|
||||
const status = await LivechatDepartment.archiveDepartment(department._id);
|
||||
if (status.modifiedCount) {
|
||||
await afterDepartmentArchived(department);
|
||||
}
|
||||
}
|
||||
|
||||
export async function unarchiveDepartment(_id: string) {
|
||||
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id'>>(_id, { projection: { _id: 1 } });
|
||||
|
||||
if (!department) {
|
||||
throw new Meteor.Error('department-not-found');
|
||||
}
|
||||
|
||||
const status = await LivechatDepartment.unarchiveDepartment(department._id);
|
||||
if (status.modifiedCount) {
|
||||
await afterDepartmentUnarchived(department);
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveDepartmentAgents(
|
||||
_id: string,
|
||||
departmentAgents: {
|
||||
upsert?: (Pick<ILivechatDepartmentAgents, 'agentId' | 'username'> & {
|
||||
count?: number;
|
||||
order?: number;
|
||||
})[];
|
||||
remove?: Pick<ILivechatDepartmentAgents, 'agentId' | 'username'>[];
|
||||
},
|
||||
) {
|
||||
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'enabled'>>(_id, { projection: { enabled: 1 } });
|
||||
if (!department) {
|
||||
throw new Meteor.Error('error-department-not-found', 'Department not found');
|
||||
}
|
||||
|
||||
return updateDepartmentAgents(_id, departmentAgents, department.enabled);
|
||||
}
|
||||
|
||||
export async function setDepartmentForGuest({ visitorId, department }: { visitorId: string; department: string }) {
|
||||
livechatLogger.debug({
|
||||
msg: 'Switching departments for visitor',
|
||||
visitorId,
|
||||
department,
|
||||
});
|
||||
|
||||
const dep = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id'>>(department, { projection: { _id: 1 } });
|
||||
if (!dep) {
|
||||
throw new Meteor.Error('invalid-department', 'Provided department does not exists');
|
||||
}
|
||||
|
||||
// Visitor is already validated at this point
|
||||
return LivechatVisitors.updateDepartmentById(visitorId, department);
|
||||
}
|
||||
|
||||
export async function removeDepartment(departmentId: string) {
|
||||
livechatLogger.debug({ msg: 'Removing department', departmentId });
|
||||
|
||||
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'businessHourId' | 'parentId'>>(departmentId, {
|
||||
projection: { _id: 1, businessHourId: 1, parentId: 1 },
|
||||
});
|
||||
if (!department) {
|
||||
throw new Error('error-department-not-found');
|
||||
}
|
||||
|
||||
const { _id } = department;
|
||||
|
||||
const ret = await LivechatDepartment.removeById(_id);
|
||||
if (ret.acknowledged !== true) {
|
||||
throw new Error('error-failed-to-delete-department');
|
||||
}
|
||||
|
||||
const removedAgents = await LivechatDepartmentAgents.findByDepartmentId(department._id, { projection: { agentId: 1 } }).toArray();
|
||||
|
||||
const actions = ['Removing department agents', 'Unsetting fallback department', 'Removing department from rooms'];
|
||||
livechatLogger.debug({
|
||||
msg: 'Post department removal actions',
|
||||
departmentId: _id,
|
||||
actions,
|
||||
});
|
||||
|
||||
const promiseResponses = await Promise.allSettled([
|
||||
LivechatDepartmentAgents.removeByDepartmentId(_id),
|
||||
LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id),
|
||||
LivechatRooms.bulkRemoveDepartmentAndUnitsFromRooms(_id),
|
||||
]);
|
||||
|
||||
promiseResponses.forEach((response, index) => {
|
||||
if (response.status === 'rejected') {
|
||||
livechatLogger.error({
|
||||
msg: 'Post removal action failed',
|
||||
actionId: actions[index],
|
||||
error: response.reason,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const { deletedCount } = promiseResponses[0].status === 'fulfilled' ? promiseResponses[0].value : { deletedCount: 0 };
|
||||
|
||||
if (deletedCount > 0) {
|
||||
removedAgents.forEach(({ _id: docId, agentId }) => {
|
||||
void notifyOnLivechatDepartmentAgentChanged(
|
||||
{
|
||||
_id: docId,
|
||||
agentId,
|
||||
departmentId: _id,
|
||||
},
|
||||
'removed',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds: removedAgents.map(({ agentId }) => agentId) });
|
||||
void Apps.self?.triggerEvent(AppEvents.IPostLivechatDepartmentRemoved, { department });
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function getRequiredDepartment(onlineRequired = true) {
|
||||
if (!onlineRequired) {
|
||||
return LivechatDepartment.findOneEnabledWithAgentsAndRegistration();
|
||||
}
|
||||
|
||||
const departments = LivechatDepartment.findEnabledWithAgentsAndRegistration();
|
||||
|
||||
for await (const dept of departments) {
|
||||
const onlineAgents = await checkOnlineForDepartment(dept._id);
|
||||
if (onlineAgents) {
|
||||
return dept;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkOnlineForDepartment(departmentId: string) {
|
||||
const depUsers = await LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { username: 1 } }).toArray();
|
||||
const onlineForDep = await Users.findOneOnlineAgentByUserList(
|
||||
depUsers.map((agent) => agent.username),
|
||||
{ projection: { _id: 1 } },
|
||||
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
|
||||
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
|
||||
);
|
||||
|
||||
return !!onlineForDep;
|
||||
}
|
||||
|
||||
export async function getOnlineForDepartment(departmentId: string) {
|
||||
const agents = await LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { username: 1 } }).toArray();
|
||||
const usernames = agents.map(({ username }) => username);
|
||||
|
||||
return Users.findOnlineUserFromList<ILivechatAgent>(
|
||||
[...new Set(usernames)],
|
||||
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
|
||||
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user