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,14 @@
import type { ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
import { LivechatDepartmentAgents } from '@rocket.chat/models';
export async function findAgentDepartments({
enabledDepartmentsOnly,
agentId,
}: {
enabledDepartmentsOnly?: boolean;
agentId: string;
}): Promise<{ departments: (ILivechatDepartmentAgents & { departmentName: string })[] }> {
return {
departments: await LivechatDepartmentAgents.findDepartmentsOfAgent(agentId, enabledDepartmentsOnly).toArray(),
};
}

View File

@@ -0,0 +1,40 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { Settings } from '@rocket.chat/models';
export async function findAppearance(): Promise<{ appearance: ISetting[] }> {
const query = {
_id: {
$in: [
'Livechat_title',
'Livechat_title_color',
'Livechat_enable_message_character_limit',
'Livechat_message_character_limit',
'Livechat_show_agent_info',
'Livechat_show_agent_email',
'Livechat_display_offline_form',
'Livechat_offline_form_unavailable',
'Livechat_offline_message',
'Livechat_offline_success_message',
'Livechat_offline_title',
'Livechat_offline_title_color',
'Livechat_offline_email',
'Livechat_conversation_finished_message',
'Livechat_registration_form',
'Livechat_name_field_registration_form',
'Livechat_email_field_registration_form',
'Livechat_registration_form_message',
'Livechat_conversation_finished_text',
'Livechat_hide_watermark',
'Livechat_background',
'Livechat_widget_position',
'Livechat_hide_system_messages',
'Omnichannel_allow_visitors_to_close_conversation',
'Livechat_hide_expand_chat',
],
},
};
return {
appearance: await Settings.find(query).toArray(),
};
}

View File

@@ -0,0 +1,9 @@
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { businessHourManager } from '../../../../lib/omnichannel/business-hour';
export async function findLivechatBusinessHour(id?: string, type?: string): Promise<Record<string, ILivechatBusinessHour>> {
return {
businessHour: (await businessHourManager.getBusinessHour(id, type)) as ILivechatBusinessHour,
};
}

View File

@@ -0,0 +1,61 @@
import type { ILivechatCustomField } from '@rocket.chat/core-typings';
import { LivechatCustomField, LivechatVisitors, LivechatRooms } from '@rocket.chat/models';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/tools';
import type { UpdateResult, Document } from 'mongodb';
export async function findLivechatCustomFields({
text,
pagination: { offset, count, sort },
}: {
text?: string;
pagination: { offset: number; count: number; sort: Record<string, 1 | -1> };
}): Promise<PaginatedResult<{ customFields: Array<ILivechatCustomField> }>> {
const query = {
...(text && {
$or: [{ label: new RegExp(escapeRegExp(text), 'i') }, { _id: new RegExp(escapeRegExp(text), 'i') }],
}),
};
const { cursor, totalCount } = LivechatCustomField.findPaginated(query, {
sort: sort || { label: 1 },
skip: offset,
limit: count,
});
const [customFields, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
customFields,
count: customFields.length,
offset,
total,
};
}
export async function findCustomFieldById({
customFieldId,
}: {
customFieldId: string;
}): Promise<{ customField: ILivechatCustomField | null }> {
return {
customField: await LivechatCustomField.findOneById(customFieldId),
};
}
export async function setCustomField(
token: string,
key: string,
value: string,
overwrite = true,
): Promise<boolean | UpdateResult | Document> {
const customField = await LivechatCustomField.findOneById(key);
if (customField) {
if (customField.scope === 'room') {
return LivechatRooms.updateDataByToken(token, key, value, overwrite);
}
return LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite);
}
return true;
}

View File

@@ -0,0 +1,197 @@
import type { ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models';
import { applyDepartmentRestrictions } from '@rocket.chat/omni-core';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/tools';
import type { Document, Filter, FilterOperators, FindOptions } from 'mongodb';
import { hasPermissionAsync } from '../../../../lib/authorization/hasPermission';
type Pagination<T extends Document> = { pagination: { offset: number; count: number; sort: FindOptions<T>['sort'] } };
type FindDepartmentParams = {
userId: string;
onlyMyDepartments?: boolean;
text?: string;
enabled?: boolean;
excludeDepartmentId?: string;
showArchived?: boolean;
} & Pagination<ILivechatDepartment>;
type FindDepartmentByIdParams = {
userId: string;
departmentId: string;
includeAgents?: boolean;
onlyMyDepartments?: boolean;
};
type FindDepartmentToAutocompleteParams = {
uid: string;
selector: {
exceptions: string[];
conditions: Filter<ILivechatDepartment>;
term: string;
};
onlyMyDepartments?: boolean;
showArchived?: boolean;
};
type FindDepartmentAgentsParams = {
userId: string;
departmentId: string;
} & Pagination<ILivechatDepartmentAgents>;
export async function findDepartments({
userId,
onlyMyDepartments = false,
text,
enabled,
excludeDepartmentId,
showArchived = false,
pagination: { offset, count, sort },
}: FindDepartmentParams): Promise<PaginatedResult<{ departments: ILivechatDepartment[] }>> {
let query: FilterOperators<ILivechatDepartment> = {
$or: [{ type: { $eq: 'd' } }, { type: { $exists: false } }],
...(!showArchived && { archived: { $ne: !showArchived } }),
...(enabled && { enabled: Boolean(enabled) }),
...(text && { name: new RegExp(escapeRegExp(text), 'i') }),
...(excludeDepartmentId && { _id: { $ne: excludeDepartmentId } }),
};
if (onlyMyDepartments) {
query = await applyDepartmentRestrictions(query, userId);
}
const { cursor, totalCount } = LivechatDepartment.findPaginated(query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
});
const [departments, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
departments,
count: departments.length,
offset,
total,
};
}
export async function findArchivedDepartments({
userId,
onlyMyDepartments = false,
text,
excludeDepartmentId,
pagination: { offset, count, sort },
}: FindDepartmentParams): Promise<PaginatedResult<{ departments: ILivechatDepartment[] }>> {
let query: FilterOperators<ILivechatDepartment> = {
$or: [{ type: { $eq: 'd' } }, { type: { $exists: false } }],
archived: { $eq: true },
...(text && { name: new RegExp(escapeRegExp(text), 'i') }),
...(excludeDepartmentId && { _id: { $ne: excludeDepartmentId } }),
};
if (onlyMyDepartments) {
query = await applyDepartmentRestrictions(query, userId);
}
const { cursor, totalCount } = LivechatDepartment.findPaginated(query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
});
const [departments, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
departments,
count: departments.length,
offset,
total,
};
}
export async function findDepartmentById({
userId,
departmentId,
includeAgents = true,
onlyMyDepartments = false,
}: FindDepartmentByIdParams): Promise<{
department: ILivechatDepartment | null;
agents?: ILivechatDepartmentAgents[];
}> {
const canViewLivechatDepartments = includeAgents && (await hasPermissionAsync(userId, 'view-livechat-departments'));
let query: FilterOperators<ILivechatDepartment> = { _id: departmentId };
if (onlyMyDepartments) {
query = await applyDepartmentRestrictions(query, userId);
}
const result = {
department: await LivechatDepartment.findOne(query),
...(includeAgents &&
canViewLivechatDepartments && {
agents: await LivechatDepartmentAgents.findByDepartmentId(departmentId).toArray(),
}),
};
return result;
}
export async function findDepartmentsToAutocomplete({
uid,
selector,
onlyMyDepartments = false,
showArchived = false,
}: FindDepartmentToAutocompleteParams): Promise<{ items: Pick<ILivechatDepartment, '_id' | 'name'>[] }> {
const { exceptions = [] } = selector;
let { conditions = {} } = selector;
if (onlyMyDepartments) {
conditions = await applyDepartmentRestrictions(conditions, uid);
}
const conditionsWithArchived = { archived: { $ne: !showArchived }, ...conditions };
const items = await LivechatDepartment.findByNameRegexWithExceptionsAndConditions(selector.term, exceptions, conditionsWithArchived, {
projection: {
_id: 1,
name: 1,
},
sort: {
name: 1,
},
}).toArray();
return {
items,
};
}
export async function findDepartmentAgents({
departmentId,
pagination: { offset, count, sort },
}: FindDepartmentAgentsParams): Promise<PaginatedResult<{ agents: ILivechatDepartmentAgents[] }>> {
const { cursor, totalCount } = LivechatDepartmentAgents.findAgentsByDepartmentId<ILivechatDepartmentAgents>(departmentId, {
sort: sort || { username: 1 },
skip: offset,
limit: count,
});
const [agents, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
agents,
count: agents.length,
offset,
total,
};
}
export async function findDepartmentsBetweenIds({
ids,
fields,
}: {
ids: string[];
fields: Record<string, unknown>;
}): Promise<{ departments: ILivechatDepartment[] }> {
const departments = await LivechatDepartment.findInIds(ids, { projection: fields }).toArray();
return { departments };
}

View File

@@ -0,0 +1,90 @@
import { LivechatInquiryStatus } from '@rocket.chat/core-typings';
import type { ILivechatInquiryRecord, IRoom, IUser } from '@rocket.chat/core-typings';
import { LivechatDepartmentAgents, LivechatDepartment, LivechatInquiry } from '@rocket.chat/models';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
import type { Filter } from 'mongodb';
import { getOmniChatSortQuery } from '../../../../../app/livechat/lib/inquiries';
import { getInquirySortMechanismSetting } from '../../../../lib/omnichannel/settings';
const agentDepartments = async (userId: IUser['_id']): Promise<string[]> => {
const agentDepartments = (await LivechatDepartmentAgents.findByAgentId(userId, { projection: { departmentId: 1 } }).toArray()).map(
({ departmentId }) => departmentId,
);
return (await LivechatDepartment.findEnabledInIds(agentDepartments, { projection: { _id: 1 } }).toArray()).map(({ _id }) => _id);
};
const applyDepartmentRestrictions = async (
userId: IUser['_id'],
filterDepartment?: string,
): Promise<{ $in: string[] } | { $exists: false } | string> => {
const allowedDepartments = await agentDepartments(userId);
if (allowedDepartments && Array.isArray(allowedDepartments) && allowedDepartments.length > 0) {
if (!filterDepartment) {
return { $in: allowedDepartments };
}
if (!allowedDepartments.includes(filterDepartment)) {
throw new Error('error-not-authorized');
}
return filterDepartment;
}
return { $exists: false };
};
export async function findInquiries({
userId,
department: filterDepartment,
status,
pagination: { offset, count, sort },
}: {
userId: IUser['_id'];
department?: string;
status?: LivechatInquiryStatus;
pagination: { offset: number; count: number; sort: Record<string, number> };
}): Promise<PaginatedResult<{ inquiries: Array<ILivechatInquiryRecord> }>> {
const department = await applyDepartmentRestrictions(userId, filterDepartment);
const defaultSort = getOmniChatSortQuery(getInquirySortMechanismSetting());
const options = {
limit: count,
skip: offset,
sort: { ...sort, ...defaultSort },
};
const filter: Filter<ILivechatInquiryRecord> = {
// V in Enum only works for numeric enums
...(status && Object.values(LivechatInquiryStatus).includes(status) && { status }),
$or: [
// Cases where this user is the default agent
{
'defaultAgent': { $exists: true },
'defaultAgent.agentId': userId,
},
// Cases with no default agent assigned yet, AND either:
// - belongs to one of user's departments, or
// - has no department (public queue)
{
defaultAgent: { $exists: false },
$or: [...(department ? [{ department }] : []), { department: { $exists: false } }],
},
],
};
const { cursor, totalCount } = LivechatInquiry.findPaginated(filter, options);
const [inquiries, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
inquiries,
count: inquiries.length,
offset,
total,
};
}
export async function findOneInquiryByRoomId({ roomId }: { roomId: IRoom['_id'] }): Promise<{ inquiry: ILivechatInquiryRecord | null }> {
return {
inquiry: await LivechatInquiry.findOneByRoomId(roomId, {}),
};
}

View File

@@ -0,0 +1,20 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { Settings } from '@rocket.chat/models';
export async function findIntegrationSettings(): Promise<{ settings: ISetting[] }> {
const settings = await Settings.findByIds([
'Livechat_webhookUrl',
'Livechat_secret_token',
'Livechat_http_timeout',
'Livechat_webhook_on_start',
'Livechat_webhook_on_close',
'Livechat_webhook_on_chat_taken',
'Livechat_webhook_on_chat_queued',
'Livechat_webhook_on_forward',
'Livechat_webhook_on_offline_msg',
'Livechat_webhook_on_visitor_message',
'Livechat_webhook_on_agent_message',
]).toArray();
return { settings };
}

View File

@@ -0,0 +1,198 @@
import type { ILivechatAgent, ILivechatDepartment, ILivechatTrigger, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { EmojiCustom, LivechatTrigger, LivechatVisitors, LivechatRooms, LivechatDepartment } from '@rocket.chat/models';
import { makeFunction } from '@rocket.chat/patch-injection';
import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../../lib/callbacks';
import { normalizeAgent } from '../../../../lib/omnichannel/Helper';
import { getInitSettings } from '../../../../lib/omnichannel/settings';
async function findTriggers(): Promise<Pick<ILivechatTrigger, '_id' | 'actions' | 'conditions' | 'runOnce'>[]> {
const triggers = await LivechatTrigger.findEnabled().toArray();
const hasLicense = License.hasModule('livechat-enterprise');
const premiumActions = ['use-external-service'];
return triggers
.filter(({ actions }) => hasLicense || actions.some((c) => !premiumActions.includes(c.name)))
.map(({ _id, actions, conditions, runOnce }) => ({
_id,
actions,
conditions,
runOnce,
}));
}
export type CheckUnitsFromUser = {
userId?: string;
businessUnit?: string;
};
export const checkUnitsFromUser = makeFunction(async (_params: CheckUnitsFromUser): Promise<void> => undefined);
async function findDepartments(
businessUnit?: string,
userId?: string,
): Promise<Pick<ILivechatDepartment, '_id' | 'name' | 'showOnRegistration' | 'showOnOfflineForm' | 'departmentsAllowedToForward'>[]> {
// TODO: check this function usage
await checkUnitsFromUser({ userId, businessUnit });
return LivechatDepartment.findEnabledWithAgentsAndBusinessUnit<
Pick<ILivechatDepartment, '_id' | 'name' | 'showOnRegistration' | 'showOnOfflineForm' | 'departmentsAllowedToForward'>
>(businessUnit, {
_id: 1,
name: 1,
showOnRegistration: 1,
showOnOfflineForm: 1,
departmentsAllowedToForward: 1,
}).toArray();
}
export function findGuest(token: string): Promise<ILivechatVisitor | null> {
return LivechatVisitors.getVisitorByToken(token);
}
export function findGuestWithoutActivity(
token: string,
): Promise<Pick<ILivechatVisitor, '_id' | 'name' | 'username' | 'token' | 'visitorEmails' | 'department'> | null> {
return LivechatVisitors.getVisitorByToken(token, { projection: { name: 1, username: 1, token: 1, visitorEmails: 1, department: 1 } });
}
export async function findRoom(token: string, rid?: string): Promise<IOmnichannelRoom | null> {
const fields = {
t: 1,
departmentId: 1,
servedBy: 1,
open: 1,
v: 1,
ts: 1,
};
if (!rid) {
return LivechatRooms.findOneByVisitorToken(token, fields);
}
return LivechatRooms.findOneByIdAndVisitorToken(rid, token, fields);
}
export async function findOpenRoom(token: string, departmentId?: string, callerId?: string): Promise<IOmnichannelRoom | undefined> {
const options = {
projection: {
departmentId: 1,
servedBy: 1,
open: 1,
},
};
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: callerId });
const rooms = departmentId
? await LivechatRooms.findOpenByVisitorTokenAndDepartmentId(token, departmentId, options, extraQuery).toArray()
: await LivechatRooms.findOpenByVisitorToken(token, options, extraQuery).toArray();
if (rooms && rooms.length > 0) {
return rooms[0];
}
}
export async function findAgent(agentId?: string): Promise<void | { hiddenInfo: boolean } | ILivechatAgent> {
return normalizeAgent(agentId);
}
export function normalizeHttpHeaderData(headers: Headers = new Headers()): {
httpHeaders: Record<string, string | string[] | undefined>;
} {
const httpHeaders = Object.fromEntries(headers.entries());
return { httpHeaders };
}
export async function settings({ businessUnit = '', userId }: { businessUnit?: string; userId?: string } = {}): Promise<
Record<string, string | number | any>
> {
const [initSettings, triggers, departments, emojis] = await Promise.all([
getInitSettings(),
findTriggers(),
findDepartments(businessUnit, userId),
EmojiCustom.find().toArray(),
]);
const sound = `${Meteor.absoluteUrl()}sounds/chime.mp3`;
return {
enabled: initSettings.Livechat_enabled,
settings: {
registrationForm: initSettings.Livechat_registration_form,
allowSwitchingDepartments: initSettings.Livechat_allow_switching_departments,
nameFieldRegistrationForm: initSettings.Livechat_name_field_registration_form,
emailFieldRegistrationForm: initSettings.Livechat_email_field_registration_form,
displayOfflineForm: initSettings.Livechat_display_offline_form,
videoCall: initSettings.Omnichannel_call_provider === 'default-provider',
fileUpload: initSettings.Livechat_fileupload_enabled && initSettings.FileUpload_Enabled,
language: initSettings.Language,
transcript: initSettings.Livechat_enable_transcript,
historyMonitorType: initSettings.Livechat_history_monitor_type,
forceAcceptDataProcessingConsent: initSettings.Livechat_force_accept_data_processing_consent,
showConnecting: initSettings.Livechat_Show_Connecting,
agentHiddenInfo: initSettings.Livechat_show_agent_info === false,
clearLocalStorageWhenChatEnded: initSettings.Livechat_clear_local_storage_when_chat_ended,
limitTextLength:
initSettings.Livechat_enable_message_character_limit &&
(initSettings.Livechat_message_character_limit || initSettings.Message_MaxAllowedSize),
hiddenSystemMessages: initSettings.Livechat_hide_system_messages,
livechatLogo: initSettings.Assets_livechat_widget_logo,
hideWatermark: initSettings.Livechat_hide_watermark || false,
visitorsCanCloseChat: initSettings.Omnichannel_allow_visitors_to_close_conversation,
},
theme: {
title: initSettings.Livechat_title,
color: initSettings.Livechat_title_color,
offlineTitle: initSettings.Livechat_offline_title,
offlineColor: initSettings.Livechat_offline_title_color,
position: initSettings.Livechat_widget_position || 'right',
background: initSettings.Livechat_background,
hideExpandChat: initSettings.Livechat_hide_expand_chat,
actionLinks: {
jitsi: [
{ icon: 'icon-videocam', i18nLabel: 'Accept' },
{ icon: 'icon-cancel', i18nLabel: 'Decline' },
],
},
},
messages: {
offlineMessage: initSettings.Livechat_offline_message,
offlineSuccessMessage: initSettings.Livechat_offline_success_message,
offlineUnavailableMessage: initSettings.Livechat_offline_form_unavailable,
conversationFinishedMessage: initSettings.Livechat_conversation_finished_message,
conversationFinishedText: initSettings.Livechat_conversation_finished_text,
transcriptMessage: initSettings.Livechat_transcript_message,
registrationFormMessage: initSettings.Livechat_registration_form_message,
dataProcessingConsentText: initSettings.Livechat_data_processing_consent_text,
},
survey: {
items: ['satisfaction', 'agentKnowledge', 'agentResposiveness', 'agentFriendliness'],
values: ['1', '2', '3', '4', '5'],
},
triggers,
departments,
resources: {
sound,
emojis,
},
};
}
export const getExtraConfigInfo = makeFunction(
async (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;
}> => options,
);
export const onCheckRoomParams = makeFunction((params: any) => params);

View File

@@ -0,0 +1,46 @@
import { LivechatRooms } from '@rocket.chat/models';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
export async function findQueueMetrics({
agentId,
includeOfflineAgents,
departmentId,
pagination: { offset, count, sort },
}: {
agentId?: string;
includeOfflineAgents?: boolean;
departmentId?: string;
pagination: { offset: number; count: number; sort: Record<string, number> };
}): Promise<
PaginatedResult<{
queue: Array<{
_id: string;
user: { _id: string; userId: string; username: string; status: string };
department: { _id: string; name: string };
chats: number;
}>;
}>
> {
const result = await LivechatRooms.getQueueMetrics({
departmentId,
agentId,
includeOfflineAgents,
options: {
sort: sort || { chats: -1 },
offset,
count,
},
});
const {
sortedResults: queue,
totalCount: [{ total } = { total: 0 }],
} = result[0];
return {
queue,
count: queue.length,
offset,
total,
};
}

View File

@@ -0,0 +1,92 @@
import type { ILivechatDepartment, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms, LivechatDepartment } from '@rocket.chat/models';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
import { callbacks } from '../../../../lib/callbacks';
export async function findRooms({
agents,
roomName,
departmentId,
open,
createdAt,
closedAt,
tags,
customFields,
onhold,
queued,
units,
query,
options: { offset, count, fields, sort },
callerId,
}: {
agents?: Array<string>;
roomName?: string;
departmentId?: string;
open?: boolean;
createdAt?: {
start?: string | undefined;
end?: string | undefined;
};
closedAt?: {
start?: string | undefined;
end?: string | undefined;
};
tags?: Array<string>;
customFields?: Record<string, string>;
onhold?: string | boolean;
queued?: string | boolean;
units?: Array<string>;
query?: Record<string, any>;
options: { offset: number; count: number; fields: Record<string, number>; sort: Record<string, number> };
callerId: string;
}): Promise<PaginatedResult<{ rooms: Array<IOmnichannelRoom> }>> {
const extraQueryBase = await callbacks.run('livechat.applyRoomRestrictions', {}, { unitsFilter: units, userId: callerId });
const extraQuery = { ...extraQueryBase, ...(query || {}) };
const { cursor, totalCount } = LivechatRooms.findRoomsWithCriteria({
agents,
roomName,
departmentId,
open,
createdAt,
closedAt,
tags,
customFields,
onhold: ['t', 'true', '1'].includes(`${onhold}`),
queued: ['t', 'true', '1'].includes(`${queued}`),
options: {
sort: sort || { ts: -1 },
offset,
count,
fields,
},
extraQuery,
});
const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]);
const isRoomWithDepartmentId = (depId: string | undefined): depId is string => !!depId;
const departmentsIds = [...new Set(rooms.map((room) => room.departmentId).filter(isRoomWithDepartmentId))];
if (departmentsIds.length) {
const departments = await LivechatDepartment.findInIds(departmentsIds, {
projection: { name: 1 },
}).toArray();
rooms.forEach((room: IOmnichannelRoom & { department?: Pick<ILivechatDepartment, '_id' | 'name'> }) => {
if (!room.departmentId) {
return;
}
const department = departments.find((dept) => dept._id === room.departmentId);
if (department) {
room.department = department;
}
});
}
return {
rooms,
count: rooms.length,
offset,
total,
};
}

View File

@@ -0,0 +1,34 @@
import type { IOmnichannelSystemMessage } from '@rocket.chat/core-typings';
import { Messages } from '@rocket.chat/models';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
const normalizeTransferHistory = ({ transferData }: IOmnichannelSystemMessage): IOmnichannelSystemMessage['transferData'] => transferData;
const removeNulls = <S>(value: S | undefined): value is S => value != null;
export async function findLivechatTransferHistory({
rid,
pagination: { offset, count, sort },
}: {
rid: string;
pagination: { offset: number; count: number; sort: Record<string, 1 | -1> };
}): Promise<PaginatedResult<{ history: IOmnichannelSystemMessage['transferData'][] }>> {
const { cursor, totalCount } = Messages.findPaginated(
{ rid, t: 'livechat_transfer_history' },
{
projection: { transferData: 1 },
sort: sort || { ts: 1 },
skip: offset,
limit: count,
},
);
const [history, total] = await Promise.all([cursor.map(normalizeTransferHistory).toArray(), totalCount]);
return {
history: history.filter(removeNulls),
count: history.length,
offset,
total,
};
}

View File

@@ -0,0 +1,35 @@
import type { ILivechatTrigger } from '@rocket.chat/core-typings';
import { LivechatTrigger } from '@rocket.chat/models';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
export async function findTriggers({
pagination: { offset, count, sort },
}: {
pagination: { offset: number; count: number; sort: Record<string, 1 | -1> };
}): Promise<PaginatedResult<{ triggers: Array<ILivechatTrigger> }>> {
const { cursor, totalCount } = LivechatTrigger.findPaginated(
{},
{
sort: sort || { name: 1 },
skip: offset,
limit: count,
},
);
const [triggers, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
triggers,
count: triggers.length,
offset,
total,
};
}
export async function findTriggerById({ triggerId }: { triggerId: string }): Promise<ILivechatTrigger | null> {
return LivechatTrigger.findOneById(triggerId);
}
export async function deleteTrigger({ triggerId }: { triggerId: string }): Promise<void> {
await LivechatTrigger.removeById(triggerId);
}

View File

@@ -0,0 +1,118 @@
import type { ILivechatAgent, IRole } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/tools';
import type { FilterOperators } from 'mongodb';
/**
* @param {IRole['_id']} role the role id
* @param {string} text
* @param {any} pagination
*/
async function findUsers({
role,
text,
onlyAvailable = false,
excludeId,
showIdleAgents = true,
pagination: { offset, count, sort },
}: {
role: IRole['_id'];
text?: string;
onlyAvailable?: boolean;
excludeId?: string;
showIdleAgents?: boolean;
pagination: { offset: number; count: number; sort: any };
}): Promise<{ users: ILivechatAgent[]; count: number; offset: number; total: number }> {
const query: FilterOperators<ILivechatAgent> = {};
const orConditions: FilterOperators<ILivechatAgent>['$or'] = [];
if (text) {
const filterReg = new RegExp(escapeRegExp(text), 'i');
orConditions.push({ $or: [{ username: filterReg }, { name: filterReg }, { 'emails.address': filterReg }] });
}
if (onlyAvailable) {
query.statusLivechat = 'available';
}
if (excludeId) {
query._id = { $ne: excludeId };
}
if (!showIdleAgents) {
orConditions.push({ $or: [{ status: { $exists: true, $ne: 'offline' }, roles: { $ne: 'bot' } }, { roles: 'bot' }] });
}
if (orConditions.length) {
query.$and = orConditions;
}
const [
{
sortedResults,
totalCount: [{ total } = { total: 0 }],
},
] = await Users.findAgentsWithDepartments(role, query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
projection: {
username: 1,
name: 1,
status: 1,
statusLivechat: 1,
emails: 1,
livechat: 1,
},
});
return {
users: sortedResults,
count: sortedResults.length,
offset,
total,
};
}
export async function findAgents({
text,
onlyAvailable = false,
excludeId,
showIdleAgents = true,
pagination: { offset, count, sort },
}: {
text?: string;
onlyAvailable: boolean;
excludeId?: string;
showIdleAgents?: boolean;
pagination: { offset: number; count: number; sort: any };
}): Promise<ReturnType<typeof findUsers>> {
return findUsers({
role: 'livechat-agent',
text,
onlyAvailable,
excludeId,
showIdleAgents,
pagination: {
offset,
count,
sort,
},
});
}
export async function findManagers({
text,
pagination: { offset, count, sort },
}: {
text?: string;
pagination: { offset: number; count: number; sort: any };
}): Promise<ReturnType<typeof findUsers>> {
return findUsers({
role: 'livechat-manager',
text,
pagination: {
offset,
count,
sort,
},
});
}

View File

@@ -0,0 +1,216 @@
import type { ILivechatVisitor, IMessage, IOmnichannelRoom, IRoom, IUser, IVisitor } from '@rocket.chat/core-typings';
import { LivechatVisitors, Messages, LivechatRooms, LivechatCustomField } from '@rocket.chat/models';
import type { FindOptions } from 'mongodb';
import { canAccessRoomAsync } from '../../../../lib/authorization/canAccessRoom';
import { callbacks } from '../../../../lib/callbacks';
export async function findVisitorInfo({ visitorId }: { visitorId: IVisitor['_id'] }) {
const visitor = await LivechatVisitors.findOneEnabledById(visitorId, { projection: { token: 0 } });
if (!visitor) {
throw new Error('visitor-not-found');
}
return {
visitor,
};
}
export async function findVisitedPages({
roomId,
pagination: { offset, count, sort },
}: {
roomId: IRoom['_id'];
pagination: { offset: number; count: number; sort: FindOptions<IMessage>['sort'] };
}) {
const room = await LivechatRooms.findOneById(roomId);
if (!room) {
throw new Error('invalid-room');
}
const { cursor, totalCount } = Messages.findPaginatedByRoomIdAndType(room._id, 'livechat_navigation_history', {
sort: sort || { ts: -1 },
skip: offset,
limit: count,
});
const [pages, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
pages,
count: pages.length,
offset,
total,
};
}
export async function findChatHistory({
userId,
roomId,
visitorId,
pagination: { offset, count, sort },
}: {
userId: IUser['_id'];
roomId: IRoom['_id'];
visitorId: IVisitor['_id'];
pagination: { offset: number; count: number; sort: FindOptions<IOmnichannelRoom>['sort'] };
}) {
const room = await LivechatRooms.findOneById(roomId);
if (!room) {
throw new Error('invalid-room');
}
if (!(await canAccessRoomAsync(room, { _id: userId }))) {
throw new Error('error-not-allowed');
}
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId });
const { cursor, totalCount } = LivechatRooms.findPaginatedByVisitorId(
visitorId,
{
sort: sort || { ts: -1 },
skip: offset,
limit: count,
},
extraQuery,
);
const [history, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
history,
count: history.length,
offset,
total,
};
}
export async function searchChats({
userId,
roomId,
visitorId,
searchText,
closedChatsOnly,
servedChatsOnly: served,
source,
pagination: { offset, count, sort },
}: {
userId: IUser['_id'];
roomId: IRoom['_id'];
visitorId: IVisitor['_id'];
searchText?: string;
closedChatsOnly?: string;
servedChatsOnly?: string;
source?: string;
pagination: { offset: number; count: number; sort: FindOptions<IOmnichannelRoom>['sort'] };
}) {
const room = await LivechatRooms.findOneById(roomId);
if (!room) {
throw new Error('invalid-room');
}
if (!(await canAccessRoomAsync(room, { _id: userId }))) {
throw new Error('error-not-allowed');
}
const options = {
sort: sort || { ts: -1 },
skip: offset,
limit: count,
};
const [total] = await LivechatRooms.findRoomsByVisitorIdAndMessageWithCriteria({
visitorId,
open: closedChatsOnly !== 'true',
served: served === 'true',
searchText,
onlyCount: true,
source,
}).toArray();
const cursor = await LivechatRooms.findRoomsByVisitorIdAndMessageWithCriteria({
visitorId,
open: closedChatsOnly !== 'true',
served: served === 'true',
searchText,
options,
source,
});
const history = await cursor.toArray();
return {
history,
count: history.length,
offset,
total: total?.count ?? 0,
};
}
export async function findVisitorsToAutocomplete({
selector,
}: {
selector: {
exceptions?: ILivechatVisitor['_id'][];
conditions?: Record<string, unknown>;
term: string;
};
}) {
const { exceptions = [], conditions = {} } = selector;
const options: FindOptions<ILivechatVisitor> = {
projection: {
_id: 1,
name: 1,
username: 1,
},
limit: 10,
sort: {
name: 1,
},
};
const items = await LivechatVisitors.findByNameRegexWithExceptionsAndConditions(selector.term, exceptions, conditions, options).toArray();
return {
items,
};
}
export async function findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({
emailOrPhone,
nameOrUsername,
pagination: { offset, count, sort },
}: {
emailOrPhone?: string;
nameOrUsername?: RegExp;
pagination: { offset: number; count: number; sort: FindOptions<IVisitor>['sort'] };
}) {
const allowedCF = await LivechatCustomField.findMatchingCustomFields('visitor', true, { projection: { _id: 1 } })
.map((cf) => cf._id)
.toArray();
const { cursor, totalCount } = await LivechatVisitors.findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField(
emailOrPhone,
nameOrUsername,
allowedCF,
{
sort: sort || { ts: -1 },
skip: offset,
limit: count,
projection: {
username: 1,
name: 1,
phone: 1,
livechatData: 1,
visitorEmails: 1,
lastChat: 1,
},
},
);
const [visitors, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
visitors,
count: visitors.length,
offset,
total,
};
}