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:
14
apps/meteor/server/api/v1/omnichannel/lib/agents.ts
Normal file
14
apps/meteor/server/api/v1/omnichannel/lib/agents.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
40
apps/meteor/server/api/v1/omnichannel/lib/appearance.ts
Normal file
40
apps/meteor/server/api/v1/omnichannel/lib/appearance.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
61
apps/meteor/server/api/v1/omnichannel/lib/customFields.ts
Normal file
61
apps/meteor/server/api/v1/omnichannel/lib/customFields.ts
Normal 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;
|
||||
}
|
||||
197
apps/meteor/server/api/v1/omnichannel/lib/departments.ts
Normal file
197
apps/meteor/server/api/v1/omnichannel/lib/departments.ts
Normal 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 };
|
||||
}
|
||||
90
apps/meteor/server/api/v1/omnichannel/lib/inquiries.ts
Normal file
90
apps/meteor/server/api/v1/omnichannel/lib/inquiries.ts
Normal 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, {}),
|
||||
};
|
||||
}
|
||||
20
apps/meteor/server/api/v1/omnichannel/lib/integrations.ts
Normal file
20
apps/meteor/server/api/v1/omnichannel/lib/integrations.ts
Normal 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 };
|
||||
}
|
||||
198
apps/meteor/server/api/v1/omnichannel/lib/livechat.ts
Normal file
198
apps/meteor/server/api/v1/omnichannel/lib/livechat.ts
Normal 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);
|
||||
46
apps/meteor/server/api/v1/omnichannel/lib/queue.ts
Normal file
46
apps/meteor/server/api/v1/omnichannel/lib/queue.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
92
apps/meteor/server/api/v1/omnichannel/lib/rooms.ts
Normal file
92
apps/meteor/server/api/v1/omnichannel/lib/rooms.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
34
apps/meteor/server/api/v1/omnichannel/lib/transfer.ts
Normal file
34
apps/meteor/server/api/v1/omnichannel/lib/transfer.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
35
apps/meteor/server/api/v1/omnichannel/lib/triggers.ts
Normal file
35
apps/meteor/server/api/v1/omnichannel/lib/triggers.ts
Normal 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);
|
||||
}
|
||||
118
apps/meteor/server/api/v1/omnichannel/lib/users.ts
Normal file
118
apps/meteor/server/api/v1/omnichannel/lib/users.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
216
apps/meteor/server/api/v1/omnichannel/lib/visitors.ts
Normal file
216
apps/meteor/server/api/v1/omnichannel/lib/visitors.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user