Files
upstream-rocketchat/apps/meteor/ee/server/models/raw/LivechatRooms.ts
upstream-mirror 30810eecd7
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
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

750 lines
17 KiB
TypeScript

import type {
ILivechatPriority,
IOmnichannelRoom,
IOmnichannelServiceLevelAgreements,
RocketChatRecordDeleted,
ReportResult,
ILivechatContact,
} from '@rocket.chat/core-typings';
import { LivechatPriorityWeight, DEFAULT_SLA_CONFIG } from '@rocket.chat/core-typings';
import type { FindPaginated, ILivechatRoomsModel } from '@rocket.chat/model-typings';
import type { Updater } from '@rocket.chat/models';
import { LivechatRoomsRaw } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/tools';
import type { FindCursor, UpdateResult, Document, FindOptions, Db, Collection, Filter, AggregationCursor, UpdateOptions } from 'mongodb';
import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred';
declare module '@rocket.chat/model-typings' {
interface ILivechatRoomsModel {
associateRoomsWithDepartmentToUnit: (departments: string[], unit: string) => Promise<void>;
removeUnitAssociationFromRooms: (unit: string) => Promise<void>;
updateDepartmentAncestorsById: (rid: string, ancestors?: string[]) => Promise<UpdateResult>;
unsetPredictedVisitorAbandonmentByRoomId(rid: string): Promise<UpdateResult>;
findAbandonedOpenRooms(date: Date, extraQuery?: Filter<IOmnichannelRoom>): FindCursor<IOmnichannelRoom>;
setPredictedVisitorAbandonmentByRoomId(roomId: string, date: Date): Promise<UpdateResult>;
getPredictedVisitorAbandonmentByRoomIdUpdateQuery(date: Date, roomUpdater: Updater<IOmnichannelRoom>): Updater<IOmnichannelRoom>;
unsetAllPredictedVisitorAbandonment(): Promise<void>;
setOnHoldByRoomId(roomId: string): Promise<UpdateResult>;
unsetOnHoldByRoomId(roomId: string): Promise<UpdateResult>;
setSlaForRoomById(
roomId: string,
sla: Pick<IOmnichannelServiceLevelAgreements, '_id' | 'dueTimeInMinutes'>,
): Promise<UpdateResult | Document>;
removeSlaFromRoomById(roomId: string): Promise<UpdateResult | Document>;
bulkRemoveSlaFromRoomsById(slaId: string): Promise<UpdateResult | Document>;
findOpenBySlaId(
slaId: string,
options: FindOptions<IOmnichannelRoom>,
extraQuery?: Filter<IOmnichannelRoom>,
): FindCursor<IOmnichannelRoom>;
setPriorityByRoomId(roomId: string, priority: Pick<ILivechatPriority, '_id' | 'sortItem'>): Promise<UpdateResult>;
unsetPriorityByRoomId(roomId: string): Promise<UpdateResult>;
countPrioritizedRooms(): Promise<number>;
countRoomsWithSla(): Promise<number>;
countRoomsWithTranscriptSent(): Promise<number>;
getConversationsBySource(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): AggregationCursor<ReportResult>;
getConversationsByStatus(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): AggregationCursor<ReportResult>;
getConversationsByDepartment(
start: Date,
end: Date,
sort: Record<string, 1 | -1>,
extraQuery: Filter<IOmnichannelRoom>,
): AggregationCursor<ReportResult>;
getConversationsByTags(
start: Date,
end: Date,
sort: Record<string, 1 | -1>,
extraQuery: Filter<IOmnichannelRoom>,
): AggregationCursor<ReportResult>;
getConversationsByAgents(
start: Date,
end: Date,
sort: Record<string, 1 | -1>,
extraQuery: Filter<IOmnichannelRoom>,
): AggregationCursor<ReportResult>;
getConversationsWithoutTagsBetweenDate(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): Promise<number>;
getTotalConversationsWithoutAgentsBetweenDate(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): Promise<number>;
getTotalConversationsWithoutDepartmentBetweenDates(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): Promise<number>;
updateMergedContactIds(
contactIdsThatWereMerged: ILivechatContact['_id'][],
newContactId: ILivechatContact['_id'],
options?: UpdateOptions,
): Promise<UpdateResult | Document>;
}
}
export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoomsModel {
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IOmnichannelRoom>>) {
super(db, trash);
}
override countPrioritizedRooms(): Promise<number> {
return this.countDocuments({ priorityId: { $exists: true } });
}
override countRoomsWithSla(): Promise<number> {
return this.countDocuments({ slaId: { $exists: true } });
}
override countRoomsWithTranscriptSent(): Promise<number> {
return this.countDocuments({ pdfTranscriptFileId: { $exists: true } });
}
override async unsetAllPredictedVisitorAbandonment(): Promise<void> {
return this.updateMany(
{
'open': true,
't': 'l',
'omnichannel.predictedVisitorAbandonmentAt': { $exists: true },
},
{
$unset: { 'omnichannel.predictedVisitorAbandonmentAt': 1 },
},
).then();
}
override setOnHoldByRoomId(roomId: string): Promise<UpdateResult> {
return this.updateOne({ _id: roomId }, { $set: { onHold: true } });
}
override unsetOnHoldByRoomId(roomId: string): Promise<UpdateResult> {
return this.updateOne({ _id: roomId }, { $unset: { onHold: 1 } });
}
override setSlaForRoomById(
roomId: string,
sla: Pick<IOmnichannelServiceLevelAgreements, '_id' | 'dueTimeInMinutes'>,
): Promise<UpdateResult | Document> {
const { _id: slaId, dueTimeInMinutes } = sla;
return this.updateOne(
{
_id: roomId,
},
{
$set: {
slaId,
estimatedWaitingTimeQueue: dueTimeInMinutes,
},
},
);
}
override removeSlaFromRoomById(roomId: string): Promise<UpdateResult | Document> {
return this.updateOne(
{
_id: roomId,
},
{
$unset: {
slaId: 1,
},
$set: {
estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE,
},
},
);
}
override bulkRemoveSlaFromRoomsById(slaId: string): Promise<UpdateResult | Document> {
return this.updateMany(
{
open: true,
t: 'l',
slaId,
},
{
$unset: { slaId: 1 },
$set: {
estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE,
},
},
);
}
override findOpenBySlaId(
slaId: string,
options: FindOptions<IOmnichannelRoom>,
extraQuery?: Filter<IOmnichannelRoom>,
): FindCursor<IOmnichannelRoom> {
const query = {
t: 'l' as const,
open: true,
slaId,
...extraQuery,
};
return this.find(query, options);
}
override async setPriorityByRoomId(roomId: string, priority: Pick<ILivechatPriority, '_id' | 'sortItem'>): Promise<UpdateResult> {
const { _id: priorityId, sortItem: priorityWeight } = priority;
return this.updateOne({ _id: roomId }, { $set: { priorityId, priorityWeight } });
}
override async unsetPriorityByRoomId(roomId: string): Promise<UpdateResult> {
return this.updateOne(
{ _id: roomId },
{
$unset: {
priorityId: 1,
},
$set: {
priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED,
},
},
);
}
override getPredictedVisitorAbandonmentByRoomIdUpdateQuery(
date: Date,
roomUpdater: Updater<IOmnichannelRoom> = this.getUpdater(),
): Updater<IOmnichannelRoom> {
return roomUpdater.set('omnichannel.predictedVisitorAbandonmentAt', date);
}
override setPredictedVisitorAbandonmentByRoomId(rid: string, willBeAbandonedAt: Date): Promise<UpdateResult> {
const query = {
_id: rid,
};
const update = {
$set: {
'omnichannel.predictedVisitorAbandonmentAt': willBeAbandonedAt,
},
};
return this.updateOne(query, update);
}
override findAbandonedOpenRooms(date: Date, extraQuery?: Filter<IOmnichannelRoom>): FindCursor<IOmnichannelRoom> {
return this.find({
'omnichannel.predictedVisitorAbandonmentAt': { $lte: date },
'waitingResponse': { $exists: false },
'closedAt': { $exists: false },
'open': true,
...extraQuery,
});
}
override async unsetPredictedVisitorAbandonmentByRoomId(roomId: string): Promise<UpdateResult> {
return this.updateOne(
{
_id: roomId,
},
{
$unset: { 'omnichannel.predictedVisitorAbandonmentAt': 1 },
},
);
}
override async associateRoomsWithDepartmentToUnit(departments: string[], unitId: string): Promise<void> {
const query = {
$and: [
{
departmentId: { $in: departments },
},
{
$or: [
{
departmentAncestors: { $exists: false },
},
{
$and: [
{
departmentAncestors: { $exists: true },
},
{
departmentAncestors: { $ne: unitId },
},
],
},
],
},
],
};
const update = { $set: { departmentAncestors: [unitId] } };
await this.updateMany(query, update);
const queryToDisassociateOldRoomsConnectedToUnit = {
departmentAncestors: unitId,
departmentId: { $nin: departments },
};
const updateToDisassociateRooms = { $unset: { departmentAncestors: 1 } };
await this.updateMany(queryToDisassociateOldRoomsConnectedToUnit, updateToDisassociateRooms);
}
override async removeUnitAssociationFromRooms(unitId: string): Promise<void> {
const query = {
departmentAncestors: unitId,
};
const update = { $unset: { departmentAncestors: 1 } };
await this.updateMany(query, update);
}
override async updateDepartmentAncestorsById(rid: string, departmentAncestors?: string[]) {
const query = {
_id: rid,
};
const update = departmentAncestors ? { $set: { departmentAncestors } } : { $unset: { departmentAncestors: 1 } };
return this.updateOne(query, update);
}
override getConversationsBySource(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): AggregationCursor<ReportResult> {
return this.col.aggregate(
[
{
$match: {
source: {
$exists: true,
},
t: 'l',
ts: {
$gte: start,
$lt: end,
},
...extraQuery,
},
},
{
$group: {
_id: {
type: '$source.type',
alias: '$source.alias',
},
value: { $sum: 1 },
},
},
{
$sort: { value: -1 },
},
{
$group: {
_id: null,
total: { $sum: '$value' },
data: {
$push: {
label: {
$ifNull: ['$_id.alias', '$_id.type'],
},
value: '$value',
},
},
},
},
{
$project: {
_id: 0,
},
},
],
{ hint: 'source_1_ts_1', readPreference: readSecondaryPreferred() },
);
}
override getConversationsByStatus(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): AggregationCursor<ReportResult> {
return this.col.aggregate(
[
{
$match: {
t: 'l',
ts: {
$gte: start,
$lt: end,
},
...extraQuery,
},
},
{
$group: {
_id: null,
total: { $sum: 1 },
open: {
$sum: {
$cond: [
{
$and: [
{ $eq: ['$open', true] },
{
$or: [{ $not: ['$onHold'] }, { $eq: ['$onHold', false] }],
},
{ $ifNull: ['$servedBy', false] },
],
},
1,
0,
],
},
},
closed: {
$sum: {
$cond: [
{
$ifNull: ['$metrics.chatDuration', false],
},
1,
0,
],
},
},
queued: {
$sum: {
$cond: [
{
$and: [
{ $eq: ['$open', true] },
{
$eq: [
{
$ifNull: ['$servedBy', null],
},
null,
],
},
],
},
1,
0,
],
},
},
onhold: {
$sum: {
$cond: [{ $eq: ['$onHold', true] }, 1, 0],
},
},
},
},
{
$project: {
total: 1,
data: [
{ label: 'Open', value: '$open' },
{ label: 'Closed', value: '$closed' },
{ label: 'Queued', value: '$queued' },
{ label: 'On_Hold', value: '$onhold' },
],
},
},
{
$unwind: '$data',
},
{
$sort: { 'data.value': -1 },
},
{
$group: {
_id: '$_id',
total: { $first: '$total' },
data: { $push: '$data' },
},
},
{
$project: {
_id: 0,
},
},
],
{ readPreference: readSecondaryPreferred() },
);
}
override getConversationsByDepartment(
start: Date,
end: Date,
sort: Record<string, 1 | -1>,
extraQuery: Filter<IOmnichannelRoom>,
): AggregationCursor<ReportResult> {
return this.col.aggregate(
[
{
$match: {
t: 'l',
departmentId: {
$exists: true,
},
ts: {
$lt: end,
$gte: start,
},
...extraQuery,
},
},
{
$group: {
_id: '$departmentId',
total: { $sum: 1 },
},
},
{
$lookup: {
from: 'rocketchat_livechat_department',
localField: '_id',
foreignField: '_id',
as: 'department',
},
},
{
$group: {
_id: {
$arrayElemAt: ['$department.name', 0],
},
total: {
$sum: '$total',
},
},
},
{
$match: {
_id: {
$ne: null,
},
},
},
{
$sort: sort || { total: 1 },
},
{
$group: {
_id: null,
total: { $sum: '$total' },
data: {
$push: {
label: '$_id',
value: '$total',
},
},
},
},
{
$project: {
_id: 0,
},
},
],
{ hint: 'departmentId_1_ts_1', readPreference: readSecondaryPreferred() },
);
}
override getTotalConversationsWithoutDepartmentBetweenDates(
start: Date,
end: Date,
extraQuery: Filter<IOmnichannelRoom>,
): Promise<number> {
return this.countDocuments({
t: 'l',
departmentId: {
$exists: false,
},
ts: {
$gte: start,
$lt: end,
},
...extraQuery,
});
}
override getConversationsByTags(
start: Date,
end: Date,
sort: Record<string, 1 | -1>,
extraQuery: Filter<IOmnichannelRoom>,
): AggregationCursor<ReportResult> {
return this.col.aggregate(
[
{
$match: {
t: 'l',
ts: {
$lt: end,
$gte: start,
},
tags: {
$exists: true,
$ne: [],
},
...extraQuery,
},
},
{
$group: {
_id: '$tags',
total: {
$sum: 1,
},
},
},
{
$unwind: '$_id',
},
{
$group: {
_id: '$_id',
total: { $sum: '$total' },
},
},
{
$sort: sort || { total: 1 },
},
{
$group: {
_id: null,
total: { $sum: '$total' },
data: {
$push: {
label: '$_id',
value: '$total',
},
},
},
},
{
$project: {
_id: 0,
},
},
],
{ hint: 'tags.0_1_ts_1', readPreference: readSecondaryPreferred() },
);
}
override getConversationsWithoutTagsBetweenDate(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): Promise<number> {
return this.countDocuments({
t: 'l',
ts: {
$gte: start,
$lt: end,
},
$or: [
{
tags: {
$exists: false,
},
},
{
tags: {
$eq: [],
},
},
],
...extraQuery,
});
}
override getConversationsByAgents(
start: Date,
end: Date,
sort: Record<string, 1 | -1>,
extraQuery: Filter<IOmnichannelRoom>,
): AggregationCursor<ReportResult> {
return this.col.aggregate(
[
{
$match: {
t: 'l',
ts: {
$gte: start,
$lt: end,
},
servedBy: {
$exists: true,
},
...extraQuery,
},
},
{
$group: {
_id: '$servedBy._id',
total: { $sum: 1 },
},
},
{
$lookup: {
from: 'users',
localField: '_id',
foreignField: '_id',
as: 'agent',
},
},
{
$set: {
agent: { $first: '$agent' },
},
},
{
$addFields: {
name: {
$ifNull: ['$agent.name', '$_id'],
},
},
},
{
$sort: sort || { name: 1 },
},
{
$group: {
_id: null,
total: { $sum: '$total' },
data: {
$push: {
label: '$name',
value: '$total',
},
},
},
},
{
$project: {
_id: 0,
},
},
],
{ hint: 'servedBy_1_ts_1', readPreference: readSecondaryPreferred() },
);
}
override getTotalConversationsWithoutAgentsBetweenDate(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): Promise<number> {
return this.countDocuments({
t: 'l',
ts: {
$gte: start,
$lt: end,
},
servedBy: {
$exists: false,
},
...extraQuery,
});
}
override updateMergedContactIds(
contactIdsThatWereMerged: ILivechatContact['_id'][],
newContactId: ILivechatContact['_id'],
options?: UpdateOptions,
): Promise<UpdateResult | Document> {
return this.updateMany({ contactId: { $in: contactIdsThatWereMerged } }, { $set: { contactId: newContactId } }, options);
}
override findClosedRoomsByContactAndSourcePaginated({
contactId,
source,
options = {},
}: {
contactId: string;
source?: string;
options?: FindOptions;
}): FindPaginated<FindCursor<IOmnichannelRoom>> {
return this.findPaginated<IOmnichannelRoom>(
{
contactId,
closedAt: { $exists: true },
...(source && {
$or: [{ 'source.type': new RegExp(escapeRegExp(source), 'i') }, { 'source.alias': new RegExp(escapeRegExp(source), 'i') }],
}),
},
options,
);
}
}