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,36 @@
import type { IUser, ILivechatContactVisitorAssociation } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { LivechatContacts, LivechatRooms, LivechatVisitors } from '@rocket.chat/models';
import { i18n } from '../../../../../../server/lib/i18n';
import { closeRoom } from '../../../../../../server/lib/omnichannel/closeRoom';
export async function changeContactBlockStatus({ block, visitor }: { visitor: ILivechatContactVisitorAssociation; block: boolean }) {
const result = await LivechatContacts.setChannelBlockStatus(visitor, block);
if (!result.modifiedCount) {
throw new Error('error-contact-not-found');
}
}
export function ensureSingleContactLicense() {
if (!License.hasModule('contact-id-verification')) {
throw new Error('error-action-not-allowed');
}
}
export async function closeBlockedRoom(association: ILivechatContactVisitorAssociation, user: IUser) {
const visitor = await LivechatVisitors.findOneById(association.visitorId);
if (!visitor) {
throw new Error('error-visitor-not-found');
}
const room = await LivechatRooms.findOneOpenByContactChannelVisitor(association);
if (!room) {
return;
}
return closeRoom({ room, visitor, comment: i18n.t('close-blocked-room-comment'), user });
}

View File

@@ -0,0 +1,62 @@
import type { ReportResult, ReportWithUnmatchingElements, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';
import mem from 'mem';
import type { Filter } from 'mongodb';
type AggParams = { start: Date; end: Date; sort: Record<string, 1 | -1>; extraQuery: Filter<IOmnichannelRoom> };
const defaultValue = { data: [], total: 0 };
export const findAllConversationsBySource = async ({ start, end, extraQuery }: Omit<AggParams, 'sort'>): Promise<ReportResult> => {
return (await LivechatRooms.getConversationsBySource(start, end, extraQuery).toArray())[0] || defaultValue;
};
export const findAllConversationsByStatus = async ({ start, end, extraQuery }: Omit<AggParams, 'sort'>): Promise<ReportResult> => {
return (await LivechatRooms.getConversationsByStatus(start, end, extraQuery).toArray())[0] || defaultValue;
};
export const findAllConversationsByDepartment = async ({
start,
end,
sort,
extraQuery,
}: AggParams): Promise<ReportWithUnmatchingElements> => {
const [result, total] = await Promise.all([
LivechatRooms.getConversationsByDepartment(start, end, sort, extraQuery).toArray(),
LivechatRooms.getTotalConversationsWithoutDepartmentBetweenDates(start, end, extraQuery),
]);
return {
...(result?.[0] || defaultValue),
unspecified: total || 0,
};
};
export const findAllConversationsByTags = async ({ start, end, sort, extraQuery }: AggParams): Promise<ReportWithUnmatchingElements> => {
const [result, total] = await Promise.all([
LivechatRooms.getConversationsByTags(start, end, sort, extraQuery).toArray(),
LivechatRooms.getConversationsWithoutTagsBetweenDate(start, end, extraQuery),
]);
return {
...(result?.[0] || defaultValue),
unspecified: total || 0,
};
};
export const findAllConversationsByAgents = async ({ start, end, sort, extraQuery }: AggParams): Promise<ReportWithUnmatchingElements> => {
const [result, total] = await Promise.all([
LivechatRooms.getConversationsByAgents(start, end, sort, extraQuery).toArray(),
LivechatRooms.getTotalConversationsWithoutAgentsBetweenDate(start, end, extraQuery),
]);
return {
...(result?.[0] || defaultValue),
unspecified: total || 0,
};
};
export const findAllConversationsBySourceCached = mem(findAllConversationsBySource, { maxAge: 60000, cacheKey: JSON.stringify });
export const findAllConversationsByStatusCached = mem(findAllConversationsByStatus, { maxAge: 60000, cacheKey: JSON.stringify });
export const findAllConversationsByDepartmentCached = mem(findAllConversationsByDepartment, { maxAge: 60000, cacheKey: JSON.stringify });
export const findAllConversationsByTagsCached = mem(findAllConversationsByTags, { maxAge: 60000, cacheKey: JSON.stringify });
export const findAllConversationsByAgentsCached = mem(findAllConversationsByAgents, { maxAge: 60000, cacheKey: JSON.stringify });

View File

@@ -0,0 +1,11 @@
export interface IPagination {
offset: number;
count: number;
sort: Record<string, any>;
}
export interface IPaginatedResponse {
count: number;
offset: number;
total: number;
}

View File

@@ -0,0 +1,45 @@
import { LivechatDepartment, LivechatDepartmentAgents, LivechatUnit } from '@rocket.chat/models';
import { helperLogger } from '../../../../lib/omnichannel/logger';
export const getDepartmentsWhichUserCanAccess = async (userId: string, includeDisabled = false): Promise<string[]> => {
const departments = await LivechatDepartmentAgents.find(
{
agentId: userId,
},
{
projection: {
departmentId: 1,
},
},
).toArray();
const monitoredDepartments = await LivechatUnit.findMonitoredDepartmentsByMonitorId(userId, includeDisabled);
const combinedDepartments = [
...departments.map((department) => department.departmentId),
...monitoredDepartments.map((department) => department._id),
];
return [...new Set(combinedDepartments)];
};
export const hasAccessToDepartment = async (userId: string, departmentId: string): Promise<boolean> => {
const department = await LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(userId, departmentId, { projection: { _id: 1 } });
if (department) {
helperLogger.debug({
msg: 'User has access to department as agent',
userId,
departmentId,
});
return true;
}
const monitorAccess = await LivechatDepartment.checkIfMonitorIsMonitoringDepartmentById(userId, departmentId);
helperLogger.debug({
msg: 'User monitor access status for department',
userId,
departmentId,
hasAccess: monitorAccess,
});
return monitorAccess;
};

View File

@@ -0,0 +1,30 @@
import { LivechatInquiry, Users, OmnichannelServiceLevelAgreements } from '@rocket.chat/models';
import { updateRoomSLA } from './sla';
export async function setSLAToInquiry({ userId, roomId, sla }: { userId: string; roomId: string; sla?: string }): Promise<void> {
const inquiry = await LivechatInquiry.findOneByRoomId(roomId, { projection: { status: 1 } });
if (!inquiry || inquiry.status !== 'queued') {
throw new Error('error-invalid-inquiry');
}
const slaData = sla && (await OmnichannelServiceLevelAgreements.findOneByIdOrName(sla));
if (!slaData) {
throw new Error('error-invalid-sla');
}
const user = await Users.findOneById(userId, { projection: { _id: 1, username: 1, name: 1 } });
if (!user?.username) {
throw new Error('error-invalid-user');
}
await updateRoomSLA(
roomId,
{
_id: user._id,
name: user.name || '',
username: user.username,
},
slaData,
);
}

View File

@@ -0,0 +1,71 @@
import type { ILivechatMonitor, IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/tools';
export async function findMonitors({
text,
pagination: { offset, count, sort },
}: {
text?: string;
pagination: {
offset: number;
count: number;
sort: {
[key: string]: 1 | -1;
};
};
}): Promise<PaginatedResult<{ monitors: ILivechatMonitor[] }>> {
const query = {};
if (text) {
const filterReg = new RegExp(escapeRegExp(text), 'i');
Object.assign(query, {
$or: [{ username: filterReg }, { name: filterReg }, { 'emails.address': filterReg }],
});
}
const { cursor, totalCount } = Users.findPaginatedUsersInRolesWithQuery<ILivechatMonitor>('livechat-monitor', query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
projection: {
username: 1,
name: 1,
status: 1,
statusLivechat: 1,
emails: 1,
livechat: 1,
},
});
const [monitors, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
monitors,
count: monitors.length,
offset,
total,
};
}
export async function findMonitorByUsername({ username }: { username: string }): Promise<IUser> {
const user = await Users.findOne(
{ username, roles: 'livechat-monitor' },
{
projection: {
username: 1,
name: 1,
status: 1,
statusLivechat: 1,
emails: 1,
livechat: 1,
},
},
);
if (!user) {
throw new Error('invalid-user');
}
return user;
}

View File

@@ -0,0 +1,73 @@
import { Apps } from '@rocket.chat/apps';
import type {
IOutboundProvider,
ValidOutboundProvider,
IOutboundMessageProviderService,
IOutboundProviderMetadata,
IOutboundMessage,
} from '@rocket.chat/core-typings';
import { ValidOutboundProviderList } from '@rocket.chat/core-typings';
import { OutboundMessageProvider } from '../../../../../../server/lib/OutboundMessageProvider';
import { getOutboundService } from '../../../../../../server/lib/omnichannel/outboundcommunication';
export class OutboundMessageProviderService implements IOutboundMessageProviderService {
private readonly provider: OutboundMessageProvider;
constructor() {
this.provider = new OutboundMessageProvider();
}
get outboundMessageProvider() {
return this.provider;
}
private isProviderValid(type: any): type is ValidOutboundProvider {
return ValidOutboundProviderList.includes(type);
}
public listOutboundProviders(type?: string): IOutboundProvider[] {
if (type !== undefined && !this.isProviderValid(type)) {
throw new Error('Invalid type');
}
return this.provider.getOutboundMessageProviders(type);
}
public getProviderMetadata(providerId: string): Promise<IOutboundProviderMetadata> {
const provider = this.provider.findOneByProviderId(providerId);
if (!provider) {
throw new Error('error-invalid-provider');
}
return this.getProviderManager().getProviderMetadata(provider.appId, provider.type);
}
private getProviderManager() {
if (!Apps.self?.isLoaded()) {
throw new Error('apps-engine-not-loaded');
}
const manager = Apps.self?.getManager()?.getOutboundCommunicationProviderManager();
if (!manager) {
throw new Error('apps-engine-not-configured-correctly');
}
return manager;
}
public sendMessage(providerId: string, message: IOutboundMessage) {
const provider = this.provider.findOneByProviderId(providerId);
if (!provider) {
throw new Error('error-invalid-provider');
}
return this.getProviderManager().sendOutboundMessage(provider.appId, provider.type, message);
}
}
export const outboundMessageProvider = new OutboundMessageProviderService();
getOutboundService.patch(() => {
return outboundMessageProvider;
});

View File

@@ -0,0 +1,120 @@
import { Message } from '@rocket.chat/core-services';
import type { ILivechatPriority, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
import { LivechatInquiry, LivechatPriority, LivechatRooms } from '@rocket.chat/models';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/tools';
import type { FindOptions } from 'mongodb';
import { notifyOnLivechatInquiryChangedByRoom, notifyOnRoomChanged } from '../../../../../../server/lib/notifyListener';
import { logger } from '../../../../lib/omnichannel/logger';
type FindPriorityParams = {
text?: string;
pagination: {
offset: number;
count: number;
sort: FindOptions<ILivechatPriority>['sort'];
};
};
export async function findPriority({
text,
pagination: { offset, count, sort },
}: FindPriorityParams): Promise<PaginatedResult<{ priorities: ILivechatPriority[] }>> {
const query = {
...(text && { $or: [{ name: new RegExp(escapeRegExp(text), 'i') }, { description: new RegExp(escapeRegExp(text), 'i') }] }),
};
const { cursor, totalCount } = LivechatPriority.findPaginated(query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
});
const [priorities, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
priorities,
count: priorities.length,
offset,
total,
};
}
export async function updatePriority(_id: string, data: Pick<ILivechatPriority, 'name'> & { reset?: boolean }): Promise<ILivechatPriority> {
if (data.name) {
// If we want to enforce translated duplicates we need to change this
const priority = await LivechatPriority.findOneNameUsingRegex(data.name, { projection: { name: 1 } });
if (priority && priority._id !== _id) {
throw new Error('error-duplicate-priority-name');
}
}
const createdResult = await LivechatPriority.updatePriority(_id, data.reset || false, data.name);
if (!createdResult) {
logger.error({ msg: 'Error updating priority: unsuccessful result from MongoDB', priorityId: _id, result: createdResult });
throw Error('error-unable-to-update-priority');
}
return createdResult;
}
export const updateRoomPriority = async (
rid: string,
user: Required<Pick<IUser, '_id' | 'username' | 'name'>>,
priorityId: string,
): Promise<void> => {
const room = await LivechatRooms.findOneById(rid);
if (!room) {
throw new Error('error-room-does-not-exist');
}
const priority = await LivechatPriority.findOneById(priorityId);
if (!priority) {
throw new Error('error-invalid-priority');
}
await Promise.all([
LivechatRooms.setPriorityByRoomId(rid, priority),
LivechatInquiry.setPriorityForRoom(rid, priority),
addPriorityChangeHistoryToRoom(room._id, user, priority),
]);
void notifyOnRoomChanged({ ...room, priorityId: priority._id, priorityWeight: priority.sortItem }, 'updated');
void notifyOnLivechatInquiryChangedByRoom(rid, 'updated');
};
export const removePriorityFromRoom = async (rid: string, user: Required<Pick<IUser, '_id' | 'username' | 'name'>>): Promise<void> => {
const room = await LivechatRooms.findOneById<Omit<IOmnichannelRoom, 'priorityId' | 'priorityWeight'>>(rid, {
projection: { priorityId: 0, priorityWeight: 0 },
});
if (!room) {
throw new Error('error-room-does-not-exist');
}
await Promise.all([
LivechatRooms.unsetPriorityByRoomId(rid),
LivechatInquiry.unsetPriorityForRoom(rid),
addPriorityChangeHistoryToRoom(rid, user),
]);
void notifyOnRoomChanged(room, 'updated');
void notifyOnLivechatInquiryChangedByRoom(rid, 'updated');
};
const addPriorityChangeHistoryToRoom = async (
roomId: string,
user: Required<Pick<IUser, '_id' | 'username' | 'name'>>,
priority?: Pick<ILivechatPriority, 'name' | 'i18n'>,
) => {
await Message.saveSystemMessage('omnichannel_priority_change_history', roomId, '', user, {
priorityData: {
definedBy: {
_id: user._id,
username: user.username,
},
...(priority && { priority }),
},
});
};

View File

@@ -0,0 +1,58 @@
import type { IOmnichannelServiceLevelAgreements, IUser } from '@rocket.chat/core-typings';
import { OmnichannelServiceLevelAgreements } from '@rocket.chat/models';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/tools';
import type { FindOptions } from 'mongodb';
import {
addSlaChangeHistoryToRoom,
removeInquiryQueueSla,
removeSlaFromRoom,
updateInquiryQueueSla,
updateRoomSlaWeights,
} from '../../../../lib/omnichannel/SlaHelper';
type FindSLAParams = {
text?: string;
pagination: {
offset: number;
count: number;
sort: FindOptions<IOmnichannelServiceLevelAgreements>['sort'];
};
};
export async function findSLA({
text,
pagination: { offset, count, sort },
}: FindSLAParams): Promise<PaginatedResult<{ sla: IOmnichannelServiceLevelAgreements[] }>> {
const query = {
...(text && { $or: [{ name: new RegExp(escapeRegExp(text), 'i') }, { description: new RegExp(escapeRegExp(text), 'i') }] }),
};
const { cursor, totalCount } = OmnichannelServiceLevelAgreements.findPaginated(query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
});
const [sla, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
sla,
count: sla.length,
offset,
total,
};
}
export const updateRoomSLA = async (
roomId: string,
user: Required<Pick<IUser, '_id' | 'username' | 'name'>>,
sla: Pick<IOmnichannelServiceLevelAgreements, '_id' | 'name' | 'dueTimeInMinutes'>,
) => {
await Promise.all([updateInquiryQueueSla(roomId, sla), updateRoomSlaWeights(roomId, sla), addSlaChangeHistoryToRoom(roomId, user, sla)]);
};
export const removeRoomSLA = async (roomId: string, user: Required<Pick<IUser, '_id' | 'username' | 'name'>>) => {
await Promise.all([removeInquiryQueueSla(roomId), removeSlaFromRoom(roomId), addSlaChangeHistoryToRoom(roomId, user)]);
};

View File

@@ -0,0 +1,127 @@
import type { ILivechatTag, FindTagsResult } from '@rocket.chat/core-typings';
import { LivechatTag } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/tools';
import type { Filter, FindOptions } from 'mongodb';
import { getDepartmentsWhichUserCanAccess } from './departments';
import { hasPermissionAsync } from '../../../../../../server/lib/authorization/hasPermission';
import { helperLogger } from '../../../../lib/omnichannel/logger';
type FindTagsParams = {
userId: string;
text?: string;
pagination: {
offset: number;
count: number;
sort: FindOptions<ILivechatTag>['sort'];
};
department?: string;
viewAll?: boolean;
};
type FindTagsByIdParams = {
userId: string;
tagId: string;
};
type FindTagsByIdResult = ILivechatTag | null;
// If viewAll is true
// -> & user has access to all tags
// -> then all tags will be returned
// -> Pages:
// - Admin > Omnichannel > Tags
// - Current chat's panel, filter by tags
// - Canned response creation
// -> & user does not have access to all tags
// -> then only public tags will be returned (unauthorized access - no page uses this)
// If viewAll is false
// -> & user has access to all tags
// -> & department is not specified
// -> only public tags will be returned (Pages: Close chat modal tag selection for chats not associated with a department being closed by manager)
// -> & department is specified
// -> only tags associated with the department will be returned (Page: Close chat modal tag selection for chats associated with a department being closed by manager)
// -> & user does not have access to all tags (same as viewAll = false & user has access to all tags)
// -> & department is not specified
// -> only public tags will be returned (Page: Close chat modal tag selection for chats not associated with a department being closed by agent)
// -> & department is specified
// -> only tags associated with the department will be returned (Page: Close chat modal tag selection for chats associated with a department being closed by agent)
export async function findTags({
userId,
text,
department,
viewAll,
pagination: { offset, count, sort },
}: FindTagsParams): Promise<FindTagsResult> {
let filteredDepartmentIds: string[] = [];
if (!(await hasPermissionAsync(userId, 'manage-livechat-tags'))) {
if (viewAll) {
viewAll = false;
}
// Get a list of all departments this user has access to and only
// return tags that are associated with those departments
filteredDepartmentIds = await getDepartmentsWhichUserCanAccess(userId);
helperLogger.debug({
msg: 'User does not have permission to manage livechat tags. Filtering tags by departments user has access to.',
userId,
accessibleDepartmentsLength: filteredDepartmentIds.length,
top5AccessibleDepartments: filteredDepartmentIds.slice(0, 5),
});
if (department && !filteredDepartmentIds.includes(department)) {
department = undefined;
}
}
if (department) {
// In certain cases, the user would only want to see tags for a specific department
// EG: When closing the chat which is associated with a specific department
// user get's to choose which tag to use to close the chat
// (only tags associated with the department should be shown)
filteredDepartmentIds = [department];
}
const query: {
$and?: Filter<ILivechatTag>[];
} = {
$and: [
...(text ? [{ $or: [{ name: new RegExp(escapeRegExp(text), 'i') }, { description: new RegExp(escapeRegExp(text), 'i') }] }] : []),
...(!viewAll
? [
{
$or: [
{ departments: { $size: 0 } },
...(filteredDepartmentIds.length ? [{ departments: { $in: filteredDepartmentIds } }] : []),
],
},
]
: []),
],
};
if (!query?.$and?.length) {
delete query.$and;
}
const { cursor, totalCount } = LivechatTag.findPaginated(query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
});
const [tags, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
tags,
count: tags.length,
offset,
total,
};
}
export async function findTagById({ tagId }: FindTagsByIdParams): Promise<FindTagsByIdResult> {
return LivechatTag.findOneById(tagId);
}

View File

@@ -0,0 +1,57 @@
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
export async function callTriggerExternalService({
url,
timeout,
fallbackMessage,
body,
headers,
}: {
url: string;
timeout: number;
fallbackMessage: string;
body: Record<string, any>;
headers: Record<string, string>;
}) {
try {
const response = await fetch(url, {
timeout: timeout || 1000,
body,
headers,
method: 'POST',
// SECURITY: Integrations can only be configured by users with enough privileges. It's ok to disable this check here.
ignoreSsrfValidation: true,
});
if (!response.ok || response.status !== 200) {
const text = await response.text();
throw new Error(text);
}
const data = await response.json();
const { contents } = data;
if (
!Array.isArray(contents) ||
!contents.length ||
!contents.every(({ msg, order }) => typeof msg === 'string' && typeof order === 'number')
) {
throw new Error('External service response does not match expected format');
}
return {
response: {
statusCode: response.status,
contents: data?.contents || [],
},
};
} catch (error: any) {
const isTimeout = error.message === 'The user aborted a request.';
return {
error: isTimeout ? 'error-timeout' : 'error-invalid-external-service-response',
response: error.message,
fallbackMessage,
};
}
}

View File

@@ -0,0 +1,92 @@
import type { IOmnichannelBusinessUnit, ILivechatUnitMonitor } from '@rocket.chat/core-typings';
import { LivechatUnitMonitors, LivechatUnit } from '@rocket.chat/models';
import { getUnitsFromUser } from '@rocket.chat/omni-core-ee';
import { escapeRegExp } from '@rocket.chat/tools';
import type { FindOptions } from 'mongodb';
export async function findUnitsOfUser({
text,
userId,
pagination: { offset, count, sort },
}: {
text?: string;
userId: string;
pagination: {
offset: number;
count: number;
sort: FindOptions<IOmnichannelBusinessUnit>['sort'];
};
}): Promise<{
units: IOmnichannelBusinessUnit[];
count: number;
offset: number;
total: number;
}> {
const filter = text && new RegExp(escapeRegExp(text), 'i');
// TODO: an aggregation woudl be faster? or nah?
const myUnits = await LivechatUnitMonitors.findByMonitorId(userId)
.map((u) => u.unitId)
.toArray();
const query = { _id: { $in: myUnits }, ...(text && { $or: [{ name: filter }] }) };
const { cursor, totalCount } = LivechatUnit.findPaginatedUnits(query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
});
const [units, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
units,
count: units.length,
offset,
total,
};
}
export async function findUnits({
text,
pagination: { offset, count, sort },
}: {
text?: string;
pagination: {
offset: number;
count: number;
sort: FindOptions<IOmnichannelBusinessUnit>['sort'];
};
}): Promise<{
units: IOmnichannelBusinessUnit[];
count: number;
offset: number;
total: number;
}> {
const filter = text && new RegExp(escapeRegExp(text), 'i');
const query = { ...(text && { $or: [{ name: filter }] }) };
const { cursor, totalCount } = LivechatUnit.findPaginatedUnits(query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
});
const [units, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
units,
count: units.length,
offset,
total,
};
}
export async function findUnitMonitors({ unitId }: { unitId: string }): Promise<ILivechatUnitMonitor[]> {
return LivechatUnitMonitors.find({ unitId }).toArray();
}
export async function findUnitById({ unitId, userId }: { unitId: string; userId: string }): Promise<IOmnichannelBusinessUnit | null> {
const unitsFromUser = await getUnitsFromUser(userId);
return LivechatUnit.findOneById(unitId, {}, { unitsFromUser });
}