From 7832a40a6da4b7555aee79261971ccca65da255c Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 13 Jun 2023 21:43:08 -0600 Subject: [PATCH] refactor: Move units check outside of model for finds (#29253) --- .changeset/breezy-drinks-switch.md | 6 ++ .../app/apps/server/bridges/livechat.ts | 7 +- .../closeOmnichannelConversations.ts | 8 +- .../app/livechat/server/api/lib/livechat.ts | 5 +- .../app/livechat/server/api/lib/rooms.ts | 4 + .../app/livechat/server/api/lib/visitors.ts | 16 ++-- .../app/livechat/server/api/v1/message.ts | 4 +- .../app/livechat/server/api/v1/visitor.ts | 51 ++++++----- .../app/livechat/server/lib/Analytics.js | 88 +++++++++++-------- .../app/livechat/server/lib/Contacts.ts | 4 +- .../app/livechat/server/lib/Livechat.js | 6 +- .../livechat/server/methods/getNextAgent.ts | 4 +- .../livechat/server/methods/registerGuest.ts | 4 +- .../server/methods/removeAllClosedRooms.ts | 4 +- .../hooks/applyDepartmentRestrictions.ts | 17 +++- .../server/hooks/applyRoomRestrictions.ts | 33 +++++++ .../livechat-enterprise/server/hooks/index.ts | 1 + .../livechat-enterprise/server/lib/Helper.ts | 7 +- .../server/lib/SlaHelper.ts | 5 +- .../server/lib/VisitorInactivityMonitor.ts | 4 +- .../server/lib/query.helper.js | 15 +--- apps/meteor/ee/server/lib/audit/methods.ts | 13 ++- .../ee/server/models/raw/LivechatRooms.ts | 67 +++++--------- apps/meteor/lib/callbacks.ts | 1 + .../meteor/server/models/raw/LivechatRooms.ts | 54 +++++++++--- .../src/models/ILivechatRoomsModel.ts | 41 ++++++--- 26 files changed, 306 insertions(+), 163 deletions(-) create mode 100644 .changeset/breezy-drinks-switch.md create mode 100644 apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts diff --git a/.changeset/breezy-drinks-switch.md b/.changeset/breezy-drinks-switch.md new file mode 100644 index 00000000000..4f077b8f35d --- /dev/null +++ b/.changeset/breezy-drinks-switch.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +refactor: Move units check outside of model for finds diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 36c380fd9cd..43790c9139a 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -18,6 +18,7 @@ import { getRoom } from '../../../livechat/server/api/lib/livechat'; import { Livechat } from '../../../livechat/server/lib/Livechat'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; +import { callbacks } from '../../../../lib/callbacks'; import { deasyncPromise } from '../../../../server/deasync/deasync'; export class AppLivechatBridge extends LivechatBridge { @@ -143,10 +144,12 @@ export class AppLivechatBridge extends LivechatBridge { let result; + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + if (departmentId) { - result = await LivechatRooms.findOpenByVisitorTokenAndDepartmentId(visitor.token, departmentId, {}).toArray(); + result = await LivechatRooms.findOpenByVisitorTokenAndDepartmentId(visitor.token, departmentId, {}, extraQuery).toArray(); } else { - result = await LivechatRooms.findOpenByVisitorToken(visitor.token, {}).toArray(); + result = await LivechatRooms.findOpenByVisitorToken(visitor.token, {}, extraQuery).toArray(); } return Promise.all((result as unknown as ILivechatRoom[]).map((room) => this.orch.getConverters()?.get('rooms').convertRoom(room))); diff --git a/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts b/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts index bb003ceb006..871c6af198c 100644 --- a/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts +++ b/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts @@ -4,6 +4,7 @@ import { LivechatRooms } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import { Livechat } from '../../../livechat/server/lib/LivechatTyped'; import { i18n } from '../../../../server/lib/i18n'; +import { callbacks } from '../../../../lib/callbacks'; type SubscribedRooms = { rid: string; @@ -11,7 +12,12 @@ type SubscribedRooms = { }; export const closeOmnichannelConversations = async (user: IUser, subscribedRooms: SubscribedRooms[]): Promise => { - const roomsInfo = await LivechatRooms.findByIds(subscribedRooms.map(({ rid }) => rid)); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const roomsInfo = await LivechatRooms.findByIds( + subscribedRooms.map(({ rid }) => rid), + {}, + extraQuery, + ); const language = settings.get('Language') || 'en'; const comment = i18n.t('Agent_deactivated', { lng: language }); diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index a732d1a52bf..1629685a3e1 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -90,9 +90,10 @@ export async function findOpenRoom(token: string, departmentId?: string): Promis }, }; + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); const rooms = departmentId - ? await LivechatRooms.findOpenByVisitorTokenAndDepartmentId(token, departmentId, options).toArray() - : await LivechatRooms.findOpenByVisitorToken(token, options).toArray(); + ? await LivechatRooms.findOpenByVisitorTokenAndDepartmentId(token, departmentId, options, extraQuery).toArray() + : await LivechatRooms.findOpenByVisitorToken(token, options, extraQuery).toArray(); if (rooms && rooms.length > 0) { return rooms[0]; } diff --git a/apps/meteor/app/livechat/server/api/lib/rooms.ts b/apps/meteor/app/livechat/server/api/lib/rooms.ts index d5b86c66ae5..b130e5c2c73 100644 --- a/apps/meteor/app/livechat/server/api/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/api/lib/rooms.ts @@ -2,6 +2,8 @@ import type { ILivechatDepartment, IOmnichannelRoom } from '@rocket.chat/core-ty 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, @@ -31,6 +33,7 @@ export async function findRooms({ onhold?: string | boolean; options: { offset: number; count: number; fields: Record; sort: Record }; }): Promise }>> { + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); const { cursor, totalCount } = LivechatRooms.findRoomsWithCriteria({ agents, roomName, @@ -47,6 +50,7 @@ export async function findRooms({ count, fields, }, + extraQuery, }); const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); diff --git a/apps/meteor/app/livechat/server/api/lib/visitors.ts b/apps/meteor/app/livechat/server/api/lib/visitors.ts index 29de95b48f4..2c989bd872a 100644 --- a/apps/meteor/app/livechat/server/api/lib/visitors.ts +++ b/apps/meteor/app/livechat/server/api/lib/visitors.ts @@ -3,6 +3,7 @@ import { LivechatVisitors, Messages, LivechatRooms, LivechatCustomField } from ' import type { FindOptions } from 'mongodb'; import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom'; +import { callbacks } from '../../../../../lib/callbacks'; export async function findVisitorInfo({ visitorId }: { visitorId: IVisitor['_id'] }) { const visitor = await LivechatVisitors.findOneById(visitorId); @@ -61,11 +62,16 @@ export async function findChatHistory({ throw new Error('error-not-allowed'); } - const { cursor, totalCount } = LivechatRooms.findPaginatedByVisitorId(visitorId, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const { cursor, totalCount } = LivechatRooms.findPaginatedByVisitorId( + visitorId, + { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }, + extraQuery, + ); const [history, total] = await Promise.all([cursor.toArray(), totalCount]); diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 450654f7bdc..a349d890a4e 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -19,6 +19,7 @@ import { normalizeMessageFileUpload } from '../../../../utils/server/functions/n import { settings } from '../../../../settings/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; import { isWidget } from '../../../../api/server/helpers/isWidget'; +import { callbacks } from '../../../../../lib/callbacks'; API.v1.addRoute( 'livechat/message', @@ -254,7 +255,8 @@ API.v1.addRoute( let visitor = await LivechatVisitors.getVisitorByToken(visitorToken, {}); let rid: string; if (visitor) { - const rooms = await LivechatRooms.findOpenByVisitorToken(visitorToken).toArray(); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const rooms = await LivechatRooms.findOpenByVisitorToken(visitorToken, {}, extraQuery).toArray(); if (rooms && rooms.length > 0) { rid = rooms[0]._id; } else { diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 6c6228ec94f..1d1291b9493 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -8,6 +8,7 @@ import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; import { Livechat } from '../../lib/Livechat'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { settings } from '../../../../settings/server'; +import { callbacks } from '../../../../../lib/callbacks'; API.v1.addRoute('livechat/visitor', { async post() { @@ -46,8 +47,9 @@ API.v1.addRoute('livechat/visitor', { let visitor = await VisitorsRaw.findOneById(visitorId, {}); if (visitor) { + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); // If it's updating an existing visitor, it must also update the roomInfo - const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token).toArray(); + const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray(); await Promise.all(rooms.map((room: IRoom) => Livechat.saveRoomInfo(room, visitor))); } @@ -97,17 +99,21 @@ API.v1.addRoute('livechat/visitor/:token', { if (!visitor) { throw new Meteor.Error('invalid-token'); } - - const rooms = await LivechatRooms.findOpenByVisitorToken(this.urlParams.token, { - projection: { - name: 1, - t: 1, - cl: 1, - u: 1, - usernames: 1, - servedBy: 1, + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const rooms = await LivechatRooms.findOpenByVisitorToken( + this.urlParams.token, + { + projection: { + name: 1, + t: 1, + cl: 1, + u: 1, + usernames: 1, + servedBy: 1, + }, }, - }).toArray(); + extraQuery, + ).toArray(); // if gdpr is enabled, bypass rooms check if (rooms?.length && !settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations')) { @@ -134,16 +140,21 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { async get() { - const rooms = await LivechatRooms.findOpenByVisitorToken(this.urlParams.token, { - projection: { - name: 1, - t: 1, - cl: 1, - u: 1, - usernames: 1, - servedBy: 1, + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const rooms = await LivechatRooms.findOpenByVisitorToken( + this.urlParams.token, + { + projection: { + name: 1, + t: 1, + cl: 1, + u: 1, + usernames: 1, + servedBy: 1, + }, }, - }).toArray(); + extraQuery, + ).toArray(); return API.v1.success({ rooms }); }, }, diff --git a/apps/meteor/app/livechat/server/lib/Analytics.js b/apps/meteor/app/livechat/server/lib/Analytics.js index 043a31b10f8..e042a5c6a4b 100644 --- a/apps/meteor/app/livechat/server/lib/Analytics.js +++ b/apps/meteor/app/livechat/server/lib/Analytics.js @@ -5,6 +5,7 @@ import { secondsToHHMMSS } from '../../../utils/server'; import { getTimezone } from '../../../utils/server/lib/getTimezone'; import { Logger } from '../../../logger/server'; import { i18n } from '../../../../server/lib/i18n'; +import { callbacks } from '../../../../lib/callbacks'; const HOURS_IN_DAY = 24; const logger = new Logger('OmnichannelAnalytics'); @@ -54,7 +55,8 @@ export const Analytics = { return; } - return this.AgentOverviewData[name](from, to, departmentId); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + return this.AgentOverviewData[name](from, to, departmentId, extraQuery); }, async getAnalyticsChartData(options) { @@ -90,6 +92,7 @@ export const Analytics = { dataPoints: [], }; + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); if (isSameDay) { // data for single day const m = moment(from); @@ -106,7 +109,7 @@ export const Analytics = { lt: moment(m).add(1, 'hours'), }; - data.dataPoints.push(await this.ChartData[name](date, departmentId)); + data.dataPoints.push(await this.ChartData[name](date, departmentId, extraQuery)); } } else { for await (const m of dayIterator(from, to)) { @@ -117,7 +120,7 @@ export const Analytics = { lt: moment(m).add(1, 'days'), }; - data.dataPoints.push(await this.ChartData[name](date, departmentId)); + data.dataPoints.push(await this.ChartData[name](date, departmentId, extraQuery)); } } @@ -144,7 +147,8 @@ export const Analytics = { const t = (s) => i18n.t(s, { lng: language }); - return this.OverviewData[name](from, to, departmentId, timezone, t); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + return this.OverviewData[name](from, to, departmentId, timezone, t, extraQuery); }, ChartData: { @@ -154,15 +158,15 @@ export const Analytics = { * * @returns {Integer} */ - Total_conversations(date, departmentId) { - return LivechatRooms.getTotalConversationsBetweenDate('l', date, { departmentId }); + Total_conversations(date, departmentId, extraQuery) { + return LivechatRooms.getTotalConversationsBetweenDate('l', date, { departmentId }, extraQuery); }, - async Avg_chat_duration(date, departmentId) { + async Avg_chat_duration(date, departmentId, extraQuery) { let total = 0; let count = 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { if (metrics && metrics.chatDuration) { total += metrics.chatDuration; count++; @@ -173,7 +177,7 @@ export const Analytics = { return Math.round(avgCD * 100) / 100; }, - async Total_messages(date, departmentId) { + async Total_messages(date, departmentId, extraQuery) { let total = 0; // we don't want to count visitor messages @@ -183,6 +187,7 @@ export const Analytics = { date, { departmentId }, extraFilter, + extraQuery, ).toArray(); allConversations.map(({ msgs }) => { if (msgs) { @@ -200,10 +205,10 @@ export const Analytics = { * * @returns {Double} */ - async Avg_first_response_time(date, departmentId) { + async Avg_first_response_time(date, departmentId, extraQuery) { let frt = 0; let count = 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { if (metrics && metrics.response && metrics.response.ft) { frt += metrics.response.ft; count++; @@ -220,10 +225,10 @@ export const Analytics = { * * @returns {Double} */ - async Best_first_response_time(date, departmentId) { + async Best_first_response_time(date, departmentId, extraQuery) { let maxFrt; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { if (metrics && metrics.response && metrics.response.ft) { maxFrt = maxFrt ? Math.min(maxFrt, metrics.response.ft) : metrics.response.ft; } @@ -242,10 +247,10 @@ export const Analytics = { * * @returns {Double} */ - async Avg_response_time(date, departmentId) { + async Avg_response_time(date, departmentId, extraQuery) { let art = 0; let count = 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { if (metrics && metrics.response && metrics.response.avg) { art += metrics.response.avg; count++; @@ -263,10 +268,10 @@ export const Analytics = { * * @returns {Double} */ - async Avg_reaction_time(date, departmentId) { + async Avg_reaction_time(date, departmentId, extraQuery) { let arnt = 0; let count = 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { if (metrics && metrics.reaction && metrics.reaction.ft) { arnt += metrics.reaction.ft; count++; @@ -307,7 +312,7 @@ export const Analytics = { * * @returns {Array[Object]} */ - async Conversations(from, to, departmentId, timezone, t = (v) => v) { + async Conversations(from, to, departmentId, timezone, t = (v) => v, extraQuery) { // TODO: most calls to db here can be done in one single call instead of one per day/hour let totalConversations = 0; // Total conversations let openConversations = 0; // open conversations @@ -337,7 +342,7 @@ export const Analytics = { lt: m.add(1, 'days'), }; // eslint-disable-next-line no-await-in-loop - const result = await LivechatRooms.getAnalyticsBetweenDate(date, { departmentId }).toArray(); + const result = await LivechatRooms.getAnalyticsBetweenDate(date, { departmentId }, extraQuery).toArray(); totalConversations += result.length; result.forEach(summarize(clonedDate)); @@ -359,7 +364,7 @@ export const Analytics = { gte: h.clone(), lt: h.add(1, 'hours'), }; - (await LivechatRooms.getAnalyticsBetweenDate(date, { departmentId }).toArray()).forEach(({ msgs }) => { + (await LivechatRooms.getAnalyticsBetweenDate(date, { departmentId }, extraQuery).toArray()).forEach(({ msgs }) => { const dayHour = h.format('H'); // @int : 0, 1, ... 23 totalMessagesInHour.set(dayHour, totalMessagesInHour.has(dayHour) ? totalMessagesInHour.get(dayHour) + msgs : msgs); }); @@ -371,7 +376,7 @@ export const Analytics = { to: utcBusiestHour >= 0 ? moment.utc().set({ hour: utcBusiestHour }).tz(timezone).format('hA') : '-', from: utcBusiestHour >= 0 ? moment.utc().set({ hour: utcBusiestHour }).subtract(1, 'hour').tz(timezone).format('hA') : '', }; - const onHoldConversations = await LivechatRooms.getOnHoldConversationsBetweenDate(from, to, departmentId); + const onHoldConversations = await LivechatRooms.getOnHoldConversationsBetweenDate(from, to, departmentId, extraQuery); return [ { @@ -412,7 +417,7 @@ export const Analytics = { * * @returns {Array[Object]} */ - async Productivity(from, to, departmentId) { + async Productivity(from, to, departmentId, extraQuery) { let avgResponseTime = 0; let firstResponseTime = 0; let avgReactionTime = 0; @@ -423,7 +428,7 @@ export const Analytics = { lt: to.add(1, 'days'), }; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { if (metrics && metrics.response && metrics.reaction) { avgResponseTime += metrics.response.avg; firstResponseTime += metrics.response.ft; @@ -491,7 +496,7 @@ export const Analytics = { * * @returns {Array(Object), Array(Object)} */ - async Total_conversations(from, to, departmentId) { + async Total_conversations(from, to, departmentId, extraQuery) { let total = 0; const agentConversations = new Map(); // stores total conversations for each agent const date = { @@ -511,9 +516,15 @@ export const Analytics = { data: [], }; - const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages('l', date, { - departmentId, - }).toArray(); + const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages( + 'l', + date, + { + departmentId, + }, + {}, + extraQuery, + ).toArray(); allConversations.map((room) => { if (room.servedBy) { this.updateMap(agentConversations, room.servedBy.username, 1); @@ -548,7 +559,7 @@ export const Analytics = { * * @returns {Array(Object), Array(Object)} */ - async Avg_chat_duration(from, to, departmentId) { + async Avg_chat_duration(from, to, departmentId, extraQuery) { const agentChatDurations = new Map(); // stores total conversations for each agent const date = { gte: from, @@ -567,7 +578,7 @@ export const Analytics = { data: [], }; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics, servedBy }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { if (servedBy && metrics && metrics.chatDuration) { if (agentChatDurations.has(servedBy.username)) { agentChatDurations.set(servedBy.username, { @@ -609,7 +620,7 @@ export const Analytics = { * * @returns {Array(Object), Array(Object)} */ - async Total_messages(from, to, departmentId) { + async Total_messages(from, to, departmentId, extraQuery) { const agentMessages = new Map(); // stores total conversations for each agent const date = { gte: from, @@ -635,6 +646,7 @@ export const Analytics = { date, { departmentId }, extraFilter, + extraQuery, ).toArray(); allConversations.map(({ servedBy, msgs }) => { if (servedBy) { @@ -663,7 +675,7 @@ export const Analytics = { * * @returns {Array(Object), Array(Object)} */ - async Avg_first_response_time(from, to, departmentId) { + async Avg_first_response_time(from, to, departmentId, extraQuery) { const agentAvgRespTime = new Map(); // stores avg response time for each agent const date = { gte: from, @@ -682,7 +694,7 @@ export const Analytics = { data: [], }; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics, servedBy }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { if (servedBy && metrics && metrics.response && metrics.response.ft) { if (agentAvgRespTime.has(servedBy.username)) { agentAvgRespTime.set(servedBy.username, { @@ -724,7 +736,7 @@ export const Analytics = { * * @returns {Array(Object), Array(Object)} */ - async Best_first_response_time(from, to, departmentId) { + async Best_first_response_time(from, to, departmentId, extraQuery) { const agentFirstRespTime = new Map(); // stores avg response time for each agent const date = { gte: from, @@ -743,7 +755,7 @@ export const Analytics = { data: [], }; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics, servedBy }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { if (servedBy && metrics && metrics.response && metrics.response.ft) { if (agentFirstRespTime.has(servedBy.username)) { agentFirstRespTime.set(servedBy.username, Math.min(agentFirstRespTime.get(servedBy.username), metrics.response.ft)); @@ -777,7 +789,7 @@ export const Analytics = { * * @returns {Array(Object), Array(Object)} */ - async Avg_response_time(from, to, departmentId) { + async Avg_response_time(from, to, departmentId, extraQuery) { const agentAvgRespTime = new Map(); // stores avg response time for each agent const date = { gte: from, @@ -796,7 +808,7 @@ export const Analytics = { data: [], }; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics, servedBy }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { if (servedBy && metrics && metrics.response && metrics.response.avg) { if (agentAvgRespTime.has(servedBy.username)) { agentAvgRespTime.set(servedBy.username, { @@ -838,7 +850,7 @@ export const Analytics = { * * @returns {Array(Object), Array(Object)} */ - async Avg_reaction_time(from, to, departmentId) { + async Avg_reaction_time(from, to, departmentId, extraQuery) { const agentAvgReactionTime = new Map(); // stores avg reaction time for each agent const date = { gte: from, @@ -857,7 +869,7 @@ export const Analytics = { data: [], }; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }).forEach(({ metrics, servedBy }) => { + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { if (servedBy && metrics && metrics.reaction && metrics.reaction.ft) { if (agentAvgReactionTime.has(servedBy.username)) { agentAvgReactionTime.set(servedBy.username, { diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts index 7c2d84c6644..74ea04fb19e 100644 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -4,6 +4,7 @@ import type { MatchKeysAndValues, OnlyFieldsOfType } from 'mongodb'; import { LivechatVisitors, Users, LivechatRooms, LivechatCustomField, LivechatInquiry, Rooms, Subscriptions } from '@rocket.chat/models'; import type { ILivechatCustomField, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; import { i18n } from '../../../utils/lib/i18n'; @@ -125,7 +126,8 @@ export const Contacts = { await LivechatVisitors.updateOne({ _id: contactId }, updateUser); - const rooms: IOmnichannelRoom[] = await LivechatRooms.findByVisitorId(contactId, {}).toArray(); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const rooms: IOmnichannelRoom[] = await LivechatRooms.findByVisitorId(contactId, {}, extraQuery).toArray(); if (rooms?.length) { for await (const room of rooms) { diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 5b4fe1a65f0..6ba4a653b21 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -285,7 +285,8 @@ export const Livechat = { Livechat.logger.debug(`Closing open chats for user ${userId}`); const user = await Users.findOneById(userId); - const openChats = LivechatRooms.findOpenByAgent(userId); + const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}); + const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery); const promises = []; await openChats.forEach((room) => { promises.push(LivechatTyped.closeRoom({ user, room, comment })); @@ -635,7 +636,8 @@ export const Livechat = { const { token } = guest; check(token, String); - const cursor = LivechatRooms.findByVisitorToken(token); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const cursor = LivechatRooms.findByVisitorToken(token, extraQuery); for await (const room of cursor) { await FileUpload.removeFilesByRoomId(room._id); await Messages.removeByRoomId(room._id); diff --git a/apps/meteor/app/livechat/server/methods/getNextAgent.ts b/apps/meteor/app/livechat/server/methods/getNextAgent.ts index e8d8cacc2a5..7d79572c2d2 100644 --- a/apps/meteor/app/livechat/server/methods/getNextAgent.ts +++ b/apps/meteor/app/livechat/server/methods/getNextAgent.ts @@ -6,6 +6,7 @@ import type { ILivechatAgent } from '@rocket.chat/core-typings'; import { Livechat } from '../lib/LivechatTyped'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { callbacks } from '../../../../lib/callbacks'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -22,7 +23,8 @@ Meteor.methods({ methodDeprecationLogger.method('livechat:getNextAgent', '7.0.0'); check(token, String); - const room = await LivechatRooms.findOpenByVisitorToken(token).toArray(); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const room = await LivechatRooms.findOpenByVisitorToken(token, {}, extraQuery).toArray(); if (room && room.length > 0) { return; diff --git a/apps/meteor/app/livechat/server/methods/registerGuest.ts b/apps/meteor/app/livechat/server/methods/registerGuest.ts index 67d2897afcd..94eddc2983d 100644 --- a/apps/meteor/app/livechat/server/methods/registerGuest.ts +++ b/apps/meteor/app/livechat/server/methods/registerGuest.ts @@ -6,6 +6,7 @@ import type { ILivechatVisitor, IRoom } from '@rocket.chat/core-typings'; import { Livechat } from '../lib/Livechat'; import { Livechat as LivechatTyped } from '../lib/LivechatTyped'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { callbacks } from '../../../../lib/callbacks'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -57,8 +58,9 @@ Meteor.methods({ }, }); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); // If it's updating an existing visitor, it must also update the roomInfo - const rooms: IRoom[] = await LivechatRooms.findOpenByVisitorToken(token).toArray(); + const rooms: IRoom[] = await LivechatRooms.findOpenByVisitorToken(token, {}, extraQuery).toArray(); await Promise.all(rooms.map((room) => Livechat.saveRoomInfo(room, visitor))); if (customFields && customFields instanceof Array) { diff --git a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts index 818b8e20126..c6e4956369f 100644 --- a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts +++ b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts @@ -5,6 +5,7 @@ import { LivechatRooms } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { Livechat } from '../lib/LivechatTyped'; +import { callbacks } from '../../../../lib/callbacks'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -26,8 +27,9 @@ Meteor.methods({ // These are not debug logs since we want to know when the action is performed Livechat.logger.info(`User ${Meteor.userId()} is removing all closed rooms`); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); const promises: Promise[] = []; - await LivechatRooms.findClosedRooms(departmentIds).forEach(({ _id }: IOmnichannelRoom) => { + await LivechatRooms.findClosedRooms(departmentIds, {}, extraQuery).forEach(({ _id }: IOmnichannelRoom) => { promises.push(Livechat.removeRoom(_id)); }); await Promise.all(promises); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts index 67e521fddc5..d8479a24ac8 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts @@ -1,14 +1,25 @@ import type { FilterOperators } from 'mongodb'; -import type { ILivechatDepartmentRecord } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../../lib/callbacks'; -import { addQueryRestrictionsToDepartmentsModel } from '../lib/query.helper'; import { hasRoleAsync } from '../../../../../app/authorization/server/functions/hasRole'; import { cbLogger } from '../lib/logger'; +import { getUnitsFromUser } from '../lib/units'; + +export const addQueryRestrictionsToDepartmentsModel = async (originalQuery: FilterOperators = {}) => { + const query: FilterOperators = { ...originalQuery, type: { $ne: 'u' } }; + + const units = await getUnitsFromUser(); + if (Array.isArray(units)) { + query.ancestors = { $in: units }; + } + + return query; +}; callbacks.add( 'livechat.applyDepartmentRestrictions', - async (originalQuery: FilterOperators = {}, { userId }: { userId?: string | null } = { userId: null }) => { + async (originalQuery: FilterOperators = {}, { userId }: { userId?: string | null } = { userId: null }) => { if (!userId || !(await hasRoleAsync(userId, 'livechat-monitor'))) { cbLogger.debug('Skipping callback. No user id provided or user is not a monitor'); return originalQuery; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts new file mode 100644 index 00000000000..0ff984da5fc --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts @@ -0,0 +1,33 @@ +import type { FilterOperators } from 'mongodb'; +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; + +import { callbacks } from '../../../../../lib/callbacks'; +import { cbLogger } from '../lib/logger'; +import { getUnitsFromUser } from '../lib/units'; + +export const restrictQuery = async (originalQuery: FilterOperators = {}) => { + const query = { ...originalQuery }; + + const units = await getUnitsFromUser(); + if (!Array.isArray(units)) { + return query; + } + + const expressions = query.$and || []; + const condition = { + $or: [{ departmentAncestors: { $in: units } }, { departmentId: { $in: units } }], + }; + query.$and = [condition, ...expressions]; + + return query; +}; + +callbacks.add( + 'livechat.applyRoomRestrictions', + async (originalQuery: FilterOperators = {}) => { + cbLogger.debug('Applying room query restrictions'); + return restrictQuery(originalQuery); + }, + callbacks.priority.HIGH, + 'livechat-apply-room-restrictions', +); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts index ee43f31a81f..3eba755b573 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts @@ -24,3 +24,4 @@ import './afterForwardChatToAgent'; import './applySimultaneousChatsRestrictions'; import './afterInquiryQueued'; import './sendPdfTranscriptOnClose'; +import './applyRoomRestrictions'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts index cfe6074b3cf..e300bbdb286 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts @@ -19,6 +19,7 @@ import { logger, helperLogger } from './logger'; import { OmnichannelQueueInactivityMonitor } from './QueueInactivityMonitor'; import { getInquirySortMechanismSetting } from '../../../../../app/livechat/server/lib/settings'; import { updateInquiryQueueSla } from './SlaHelper'; +import { callbacks } from '../../../../../lib/callbacks'; type QueueInfo = { message: { @@ -202,8 +203,9 @@ export const updatePredictedVisitorAbandonment = async () => { await LivechatRooms.unsetAllPredictedVisitorAbandonment(); } else { // Eng day: use a promise queue to update the predicted visitor abandonment time instead of all at once + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); const promisesArray: Promise[] = []; - await LivechatRooms.findOpen().forEach((room) => { + await LivechatRooms.findOpen(extraQuery).forEach((room) => { promisesArray.push(setPredictedVisitorAbandonmentTime(room)); }); @@ -238,7 +240,8 @@ export const updateSLAInquiries = async (sla?: Pick[] = []; - await LivechatRooms.findOpenBySlaId(slaId, {}).forEach((room) => { + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + await LivechatRooms.findOpenBySlaId(slaId, {}, extraQuery).forEach((room) => { promises.push(updateInquiryQueueSla(room._id, sla)); }); await Promise.allSettled(promises); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts index a47dd6027eb..7e199b55bfa 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts @@ -2,8 +2,11 @@ import type { IOmnichannelServiceLevelAgreements, IUser } from '@rocket.chat/cor import { LivechatInquiry, LivechatRooms } from '@rocket.chat/models'; import { Message } from '@rocket.chat/core-services'; +import { callbacks } from '../../../../../lib/callbacks'; + export const removeSLAFromRooms = async (slaId: string) => { - const openRooms = await LivechatRooms.findOpenBySlaId(slaId, { projection: { _id: 1 } }).toArray(); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const openRooms = await LivechatRooms.findOpenBySlaId(slaId, { projection: { _id: 1 } }, extraQuery).toArray(); if (openRooms.length) { const openRoomIds: string[] = openRooms.map(({ _id }) => _id); await LivechatInquiry.bulkUnsetSla(openRoomIds); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts index 2e5e31c1467..376b0783f95 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts @@ -6,6 +6,7 @@ import { settings } from '../../../../../app/settings/server'; import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped'; import { LivechatEnterprise } from './LivechatEnterprise'; import { i18n } from '../../../../../server/lib/i18n'; +import { callbacks } from '../../../../../lib/callbacks'; import { schedulerLogger } from './logger'; import type { MainLogger } from '../../../../../server/lib/logger/getPino'; @@ -141,8 +142,9 @@ export class VisitorInactivityMonitor { return; } + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); const promises: Promise[] = []; - await LivechatRooms.findAbandonedOpenRooms(new Date()).forEach((room) => { + await LivechatRooms.findAbandonedOpenRooms(new Date(), extraQuery).forEach((room) => { switch (action) { case 'close': { this.logger.debug(`Closing room ${room._id}`); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/query.helper.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/query.helper.js index db6c30278ef..0daaa9709d5 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/query.helper.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/query.helper.js @@ -2,10 +2,10 @@ import { getUnitsFromUser } from './units'; // TODO: We need to add a new index in the departmentAncestors field -export const addQueryRestrictionsToRoomsModel = (originalQuery = {}) => { +export const addQueryRestrictionsToRoomsModel = async (originalQuery = {}) => { const query = { ...originalQuery }; - const units = Promise.await(getUnitsFromUser()); + const units = await getUnitsFromUser(); if (!Array.isArray(units)) { return query; } @@ -17,14 +17,3 @@ export const addQueryRestrictionsToRoomsModel = (originalQuery = {}) => { query.$and = [condition, ...expressions]; return query; }; - -export const addQueryRestrictionsToDepartmentsModel = async (originalQuery = {}) => { - const query = { ...originalQuery, type: { $ne: 'u' } }; - - const units = await getUnitsFromUser(); - if (Array.isArray(units)) { - query.ancestors = { $in: units }; - } - - return query; -}; diff --git a/apps/meteor/ee/server/lib/audit/methods.ts b/apps/meteor/ee/server/lib/audit/methods.ts index 6f4ac3b0c86..9236f3aa664 100644 --- a/apps/meteor/ee/server/lib/audit/methods.ts +++ b/apps/meteor/ee/server/lib/audit/methods.ts @@ -11,6 +11,7 @@ import { hasPermissionAsync } from '../../../../app/authorization/server/functio import { updateCounter } from '../../../../app/statistics/server'; import { isTruthy } from '../../../../lib/isTruthy'; import { i18n } from '../../../../server/lib/i18n'; +import { callbacks } from '../../../../lib/callbacks'; const getValue = (room: IRoom | null) => room && { rids: [room._id], name: room.name }; @@ -43,9 +44,15 @@ const getRoomInfoByAuditParams = async ({ if (type === 'l') { console.warn('Deprecation Warning! This method will be removed in the next version (4.0.0)'); - const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId(visitor, agent, { - projection: { _id: 1 }, - }).toArray(); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId( + visitor, + agent, + { + projection: { _id: 1 }, + }, + extraQuery, + ).toArray(); return rooms?.length ? { rids: rooms.map(({ _id }) => _id), name: i18n.t('Omnichannel') } : undefined; } }; diff --git a/apps/meteor/ee/server/models/raw/LivechatRooms.ts b/apps/meteor/ee/server/models/raw/LivechatRooms.ts index 847bdb6ea1a..163e04192bb 100644 --- a/apps/meteor/ee/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/ee/server/models/raw/LivechatRooms.ts @@ -6,7 +6,7 @@ import type { } from '@rocket.chat/core-typings'; import { LivechatPriorityWeight, DEFAULT_SLA_CONFIG } from '@rocket.chat/core-typings'; import type { ILivechatRoomsModel } from '@rocket.chat/model-typings'; -import type { FindCursor, UpdateResult, Document, FindOptions, Db, Collection, UpdateOptions, Filter, UpdateFilter } from 'mongodb'; +import type { FindCursor, UpdateResult, Document, FindOptions, Db, Collection, Filter } from 'mongodb'; import { LivechatRoomsRaw } from '../../../../server/models/raw/LivechatRooms'; import { queriesLogger } from '../../../app/livechat-enterprise/server/lib/logger'; @@ -18,20 +18,23 @@ declare module '@rocket.chat/model-typings' { removeUnitAssociationFromRooms: (unit: string) => Promise; updateDepartmentAncestorsById: (rid: string, ancestors?: string[]) => Promise; unsetPredictedVisitorAbandonmentByRoomId(rid: string): Promise; - findAbandonedOpenRooms(date: Date): FindCursor; + findAbandonedOpenRooms(date: Date, extraQuery?: Filter): FindCursor; setPredictedVisitorAbandonmentByRoomId(roomId: string, date: Date): Promise; unsetAllPredictedVisitorAbandonment(): Promise; setOnHoldByRoomId(roomId: string): Promise; unsetOnHoldByRoomId(roomId: string): Promise; unsetOnHoldAndPredictedVisitorAbandonmentByRoomId(roomId: string): Promise; - findOpenRoomsByPriorityId(priorityId: string): FindCursor; setSlaForRoomById( roomId: string, sla: Pick, ): Promise; removeSlaFromRoomById(roomId: string): Promise; bulkRemoveSlaFromRoomsById(slaId: string): Promise; - findOpenBySlaId(slaId: string, options: FindOptions): FindCursor; + findOpenBySlaId( + slaId: string, + options: FindOptions, + extraQuery?: Filter, + ): FindCursor; setPriorityByRoomId(roomId: string, priority: Pick): Promise; unsetPriorityByRoomId(roomId: string): Promise; } @@ -128,11 +131,16 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo ); } - findOpenBySlaId(slaId: string, options: FindOptions): FindCursor { - const query: Filter = { - t: 'l', + findOpenBySlaId( + slaId: string, + options: FindOptions, + extraQuery?: Filter, + ): FindCursor { + const query = { + t: 'l' as const, open: true, slaId, + ...extraQuery, }; return this.find(query, options); @@ -158,16 +166,6 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo ); } - findOpenRoomsByPriorityId(priorityId: string): FindCursor { - const query: Filter = { - t: 'l', - open: true, - priorityId, - }; - - return this.find(query); - } - setPredictedVisitorAbandonmentByRoomId(rid: string, willBeAbandonedAt: Date): Promise { const query = { _id: rid, @@ -181,12 +179,13 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo return this.updateOne(query, update); } - findAbandonedOpenRooms(date: Date): FindCursor { + findAbandonedOpenRooms(date: Date, extraQuery?: Filter): FindCursor { return this.find({ 'omnichannel.predictedVisitorAbandonmentAt': { $lte: date }, 'waitingResponse': { $exists: false }, 'closedAt': { $exists: false }, 'open': true, + ...extraQuery, }); } @@ -266,34 +265,16 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo return this.updateOne(query, update); } - find(...args: Parameters): FindCursor { - const [query, ...restArgs] = args; - const restrictedQuery = addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.find', query: restrictedQuery }); - return super.find(restrictedQuery, ...restArgs); - } - - findPaginated(...args: Parameters): any { - const [query, ...restArgs] = args; - const restrictedQuery = addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.findPaginated', query: restrictedQuery }); - return super.findPaginated(restrictedQuery, ...restArgs); - } - /** @deprecated Use updateOne or updateMany instead */ - update(...args: Parameters): ReturnType { + async update(...args: Parameters) { const [query, ...restArgs] = args; - const restrictedQuery = addQueryRestrictionsToRoomsModel(query); + const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); queriesLogger.debug({ msg: 'LivechatRoomsRawEE.update', query: restrictedQuery }); return super.update(restrictedQuery, ...restArgs); } - updateOne( - query: Filter, - update: UpdateFilter, - opts?: UpdateOptions, - extraOpts?: { bypassUnits?: boolean }, - ): ReturnType { + async updateOne(...args: [...Parameters, { bypassUnits?: boolean }?]) { + const [query, update, opts, extraOpts] = args; if (extraOpts?.bypassUnits) { // When calling updateOne from a service, we cannot call the meteor code inside the query restrictions // So the solution now is to pass a bypassUnits flag to the updateOne method which prevents checking @@ -301,14 +282,14 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo // We need to find a way of remove the meteor dependency when fetching units, and then, we can remove this flag return super.updateOne(query, update, opts); } - const restrictedQuery = addQueryRestrictionsToRoomsModel(query); + const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateOne', query: restrictedQuery }); return super.updateOne(restrictedQuery, update, opts); } - updateMany(...args: Parameters): ReturnType { + async updateMany(...args: Parameters) { const [query, ...restArgs] = args; - const restrictedQuery = addQueryRestrictionsToRoomsModel(query); + const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateMany', query: restrictedQuery }); return super.updateMany(restrictedQuery, ...restArgs); } diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index e93392dbf8d..a1681cbaac4 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -173,6 +173,7 @@ type ChainedCallbackSignatures = { query: FilterOperators, params: { userId: IUser['_id'] }, ) => FilterOperators; + 'livechat.applyRoomRestrictions': (query: FilterOperators) => FilterOperators; 'livechat.onMaxNumberSimultaneousChatsReached': (inquiry: ILivechatInquiryRecord) => ILivechatInquiryRecord; 'on-business-hour-start': (params: { BusinessHourBehaviorClass: { new (): IBusinessHourBehavior } }) => { BusinessHourBehaviorClass: { new (): IBusinessHourBehavior }; diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index a53f3a19cb8..e71fbe91859 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -1084,18 +1084,20 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.col.aggregate(params, { readPreference: readSecondaryPreferred() }); } - findByVisitorId(visitorId: string, options: FindOptions) { + findByVisitorId(visitorId: string, options: FindOptions, extraQuery: Filter = {}) { const query: Filter = { 't': 'l', 'v._id': visitorId, + ...extraQuery, }; return this.find(query, options); } - findPaginatedByVisitorId(visitorId: string, options: FindOptions) { + findPaginatedByVisitorId(visitorId: string, options: FindOptions, extraQuery: Filter = {}) { const query: Filter = { 't': 'l', 'v._id': visitorId, + ...extraQuery, }; return this.findPaginated(query, options); } @@ -1203,6 +1205,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive roomIds, onhold, options = {}, + extraQuery = {}, }: { agents?: string[]; roomName?: string; @@ -1217,9 +1220,11 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive roomIds?: string[]; onhold?: boolean; options?: { offset?: number; count?: number; sort?: { [k: string]: SortDirection } }; + extraQuery?: Filter; }) { const query: Filter = { t: 'l', + ...extraQuery, ...(agents && { $or: [{ 'servedBy._id': { $in: agents } }, { 'servedBy.username': { $in: agents } }], }), @@ -1407,8 +1412,8 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.updateOne({ _id: roomId }, { $set: { departmentId } }); } - findOpen() { - return this.find({ t: 'l', open: true }); + findOpen(extraQuery = {}) { + return this.find({ t: 'l', open: true, ...extraQuery }); } setAutoTransferOngoingById(roomId: string) { @@ -1745,7 +1750,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.find(query, options); } - findByIds(ids: string[], fields: FindOptions['projection']) { + findByIds(ids: string[], fields: FindOptions['projection'], extraQuery: Filter = {}) { const options: FindOptions = {}; if (fields) { @@ -1755,6 +1760,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive const query: Filter = { t: 'l', _id: { $in: ids }, + ...extraQuery, }; return this.find(query, options); @@ -1869,11 +1875,12 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return livechatCount.value; } - findOpenByVisitorToken(visitorToken: string, options: FindOptions = {}) { + findOpenByVisitorToken(visitorToken: string, options: FindOptions = {}, extraQuery: Filter = {}) { const query: Filter = { 't': 'l', 'open': true, 'v.token': visitorToken, + ...extraQuery, }; return this.find(query, options); @@ -1906,31 +1913,44 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.findOne(query, options); } - findOpenByVisitorTokenAndDepartmentId(visitorToken: string, departmentId: string, options: FindOptions = {}) { + findOpenByVisitorTokenAndDepartmentId( + visitorToken: string, + departmentId: string, + options: FindOptions = {}, + extraQuery: Filter = {}, + ) { const query: Filter = { 't': 'l', 'open': true, 'v.token': visitorToken, departmentId, + ...extraQuery, }; return this.find(query, options); } - findByVisitorToken(visitorToken: string) { + findByVisitorToken(visitorToken: string, extraQuery: Filter = {}) { const query: Filter = { 't': 'l', 'v.token': visitorToken, + ...extraQuery, }; return this.find(query); } - findByVisitorIdAndAgentId(visitorId?: string, agentId?: string, options: FindOptions = {}) { + findByVisitorIdAndAgentId( + visitorId?: string, + agentId?: string, + options: FindOptions = {}, + extraQuery: Filter = {}, + ) { const query: Filter = { t: 'l', ...(visitorId && { 'v._id': visitorId }), ...(agentId && { 'servedBy._id': agentId }), + ...extraQuery, }; return this.find(query, options); @@ -1947,12 +1967,13 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.findOne(query, options); } - findClosedRooms(departmentIds?: string[], options: FindOptions = {}) { + findClosedRooms(departmentIds?: string[], options: FindOptions = {}, extraQuery: Filter = {}) { const query: Filter = { t: 'l', open: { $exists: false }, closedAt: { $exists: true }, ...(Array.isArray(departmentIds) && departmentIds.length > 0 && { departmentId: { $in: departmentIds } }), + ...extraQuery, }; return this.find(query, options); @@ -2071,7 +2092,12 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.col.countDocuments(query); } - getAnalyticsMetricsBetweenDate(t: 'l', date: { gte: Date; lt: Date }, { departmentId }: { departmentId?: string } = {}) { + getAnalyticsMetricsBetweenDate( + t: 'l', + date: { gte: Date; lt: Date }, + { departmentId }: { departmentId?: string } = {}, + extraQuery: Document = {}, + ) { const query: Filter = { t, ts: { @@ -2079,6 +2105,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive $lt: new Date(date.lt), // ISODate, ts < date.lt }, ...(departmentId && departmentId !== 'undefined' && { departmentId }), + ...extraQuery, }; return this.find(query, { @@ -2091,6 +2118,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive date: { gte: Date; lt: Date }, { departmentId }: { departmentId?: string } = {}, extraQuery: Document = {}, + extraMatchers: Document = {}, ) { return this.col.aggregate>( [ @@ -2102,6 +2130,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive $lt: new Date(date.lt), // ISODate, ts < date.lt }, ...(departmentId && departmentId !== 'undefined' && { departmentId }), + ...extraMatchers, }, }, { $addFields: { roomId: '$_id' } }, @@ -2246,11 +2275,12 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ); } - findOpenByAgent(userId: string) { + findOpenByAgent(userId: string, extraQuery: Filter = {}) { const query: Filter = { 't': 'l', 'open': true, 'servedBy._id': userId, + ...extraQuery, }; return this.find(query); diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index ad5d98b1305..0c8b53c7150 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -1,5 +1,5 @@ import type { IMessage, IOmnichannelRoom, IOmnichannelRoomClosingInfo, ISetting, IVisitor } from '@rocket.chat/core-typings'; -import type { FindCursor, UpdateResult, AggregationCursor, Document, FindOptions, DeleteResult } from 'mongodb'; +import type { FindCursor, UpdateResult, AggregationCursor, Document, FindOptions, DeleteResult, Filter } from 'mongodb'; import type { FindPaginated } from '..'; import type { IBaseModel } from './IBaseModel'; @@ -62,9 +62,9 @@ export interface ILivechatRoomsModel extends IBaseModel { findAllAverageOfServiceTime(params: Period & WithDepartment & WithOnlyCount & WithOptions): any; - findByVisitorId(visitorId: any, options: any): any; + findByVisitorId(visitorId: any, options: any, extraQuery?: any): any; - findPaginatedByVisitorId(visitorId: any, options: any): any; + findPaginatedByVisitorId(visitorId: any, options: any, extraQuery?: any): any; findRoomsByVisitorIdAndMessageWithCriteria(params: { visitorId: any; @@ -90,6 +90,7 @@ export interface ILivechatRoomsModel extends IBaseModel { roomIds?: any; onhold: any; options?: any; + extraQuery?: any; }): FindPaginated>; getOnHoldConversationsBetweenDate(from: any, to: any, departmentId: any): any; @@ -100,7 +101,7 @@ export interface ILivechatRoomsModel extends IBaseModel { setDepartmentByRoomId(roomId: any, departmentId: any): any; - findOpen(): FindCursor; + findOpen(extraQuery?: Filter): FindCursor; setAutoTransferOngoingById(roomId: string): Promise; @@ -130,7 +131,11 @@ export interface ILivechatRoomsModel extends IBaseModel { data: { _id: string; topic: string; tags: string[]; livechatData?: Record } & Record, ): Promise; findById(_id: string, fields?: FindOptions['projection']): FindCursor; - findByIds(ids: string[], fields?: FindOptions['projection']): FindCursor; + findByIds( + ids: string[], + fields?: FindOptions['projection'], + extraQuery?: Filter, + ): FindCursor; findOneByIdAndVisitorToken( _id: string, visitorToken: string, @@ -156,7 +161,11 @@ export interface ILivechatRoomsModel extends IBaseModel { findOneLastServedAndClosedByVisitorToken(visitorToken: string, options?: FindOptions): Promise; findOneByVisitorToken(visitorToken: string, fields?: FindOptions['projection']): Promise; updateRoomCount(): Promise; - findOpenByVisitorToken(visitorToken: string, options?: FindOptions): FindCursor; + findOpenByVisitorToken( + visitorToken: string, + options?: FindOptions, + extraQuery?: Filter, + ): FindCursor; findOneOpenByVisitorToken(visitorToken: string, options?: FindOptions): Promise; findOneOpenByVisitorTokenAndDepartmentIdAndSource( visitorToken: string, @@ -168,15 +177,25 @@ export interface ILivechatRoomsModel extends IBaseModel { visitorToken: string, departmentId: string, options?: FindOptions, + extraQuery?: Filter, + ): FindCursor; + findByVisitorToken(visitorToken: string, extraQuery?: Filter): FindCursor; + findByVisitorIdAndAgentId( + visitorId?: string, + agentId?: string, + options?: FindOptions, + extraQuery?: Filter, ): FindCursor; - findByVisitorToken(visitorToken: string): FindCursor; - findByVisitorIdAndAgentId(visitorId?: string, agentId?: string, options?: FindOptions): FindCursor; findOneOpenByRoomIdAndVisitorToken( roomId: string, visitorToken: string, options?: FindOptions, ): Promise; - findClosedRooms(departmentIds?: string[], options?: FindOptions): FindCursor; + findClosedRooms( + departmentIds?: string[], + options?: FindOptions, + extraQuery?: Filter, + ): FindCursor; setResponseByRoomId(roomId: string, response: { user: { _id: string; username: string } }): Promise; setNotResponseByRoomId(roomId: string): Promise; setAgentLastMessageTs(roomId: string): Promise; @@ -190,18 +209,20 @@ export interface ILivechatRoomsModel extends IBaseModel { t: 'l', date: { gte: Date; lt: Date }, data?: { departmentId: string }, + extraQuery?: Filter, ): FindCursor>; getAnalyticsMetricsBetweenDateWithMessages( t: string, date: { gte: Date; lt: Date }, data?: { departmentId: string }, extraQuery?: Document, + extraMatchers?: Document, ): AggregationCursor>; getAnalyticsBetweenDate( date: { gte: Date; lt: Date }, data?: { departmentId: string }, ): AggregationCursor>; - findOpenByAgent(userId: string): FindCursor; + findOpenByAgent(userId: string, extraQuery?: Filter): FindCursor; changeAgentByRoomId(roomId: string, newAgent: { agentId: string; username: string }): Promise; changeDepartmentIdByRoomId(roomId: string, departmentId: string): Promise; saveCRMDataByRoomId(roomId: string, crmData: unknown): Promise;