From 0b4f3d3c2745ded2c3d299aa6aef01e107cb2de0 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 18 Jul 2025 16:42:08 -0600 Subject: [PATCH] fix: Missing notifiers for priorities & some inquiry changes (#36449) --- .changeset/sweet-dingos-decide.md | 7 +++++ .../app/lib/server/lib/notifyListener.ts | 30 ++++++++++++++----- apps/meteor/app/livechat/server/lib/Helper.ts | 8 +++++ .../app/livechat/server/lib/closeRoom.ts | 5 ++++ apps/meteor/app/livechat/server/lib/rooms.ts | 4 +++ .../roomAccessValidator.compatibility.ts | 1 + .../server/api/lib/priorities.ts | 15 ++++++++-- .../livechat-enterprise/server/api/lib/sla.ts | 2 +- .../server/hooks/beforeRoutingChat.ts | 4 ++- .../server/lib/SlaHelper.ts | 11 ++++++- .../server/services/omnichannel/queue.ts | 6 +++- .../src/models/ILivechatInquiryModel.ts | 2 +- packages/models/src/models/LivechatInquiry.ts | 4 +-- 13 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 .changeset/sweet-dingos-decide.md diff --git a/.changeset/sweet-dingos-decide.md b/.changeset/sweet-dingos-decide.md new file mode 100644 index 00000000000..cf806aba0f6 --- /dev/null +++ b/.changeset/sweet-dingos-decide.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +"@rocket.chat/models": patch +--- + +Fixes priorities, sla changes & inquiries not being propagated when change streams were not being used diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index 0ff13deaf0d..2b5c10caa66 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -250,17 +250,24 @@ export const notifyOnLivechatInquiryChanged = withDbWatcherCheck( export const notifyOnLivechatInquiryChangedById = withDbWatcherCheck( async ( - id: ILivechatInquiryRecord['_id'], + ids: ILivechatInquiryRecord['_id'] | ILivechatInquiryRecord['_id'][], clientAction: ClientAction = 'updated', diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, ): Promise => { - const inquiry = clientAction === 'removed' ? await LivechatInquiry.trashFindOneById(id) : await LivechatInquiry.findOneById(id); + const eligibleIds = Array.isArray(ids) ? ids : [ids]; - if (!inquiry) { + const items = + clientAction === 'removed' + ? LivechatInquiry.trashFind({ _id: { $in: eligibleIds } }) + : LivechatInquiry.find({ _id: { $in: eligibleIds } }); + + if (!items) { return; } - void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); + for await (const inquiry of items) { + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); + } }, ); @@ -280,17 +287,24 @@ export const notifyOnLivechatInquiryChangedByVisitorIds = withDbWatcherCheck( export const notifyOnLivechatInquiryChangedByRoom = withDbWatcherCheck( async ( - rid: ILivechatInquiryRecord['rid'], + rids: ILivechatInquiryRecord['rid'] | ILivechatInquiryRecord['rid'][], clientAction: ClientAction = 'updated', diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, ): Promise => { - const inquiry = await LivechatInquiry.findOneByRoomId(rid, {}); + const eligibleIds = Array.isArray(rids) ? rids : [rids]; - if (!inquiry) { + const items = + clientAction === 'removed' + ? LivechatInquiry.trashFind({ rid: { $in: eligibleIds } }) + : LivechatInquiry.find({ rid: { $in: eligibleIds } }); + + if (!items) { return; } - void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); + for await (const inquiry of items) { + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); + } }, ); diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 65ae49fb3b0..8d70abff5aa 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -55,6 +55,8 @@ import { notifyOnSubscriptionChangedById, notifyOnSubscriptionChangedByRoomId, notifyOnSubscriptionChanged, + notifyOnRoomChangedById, + notifyOnLivechatInquiryChangedByRoom, } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; @@ -555,6 +557,12 @@ export const updateChatDepartment = async ({ Subscriptions.changeDepartmentByRoomId(rid, newDepartmentId), ]); + if (responses[0].modifiedCount) { + void notifyOnRoomChangedById(rid); + } + if (responses[1].modifiedCount) { + void notifyOnLivechatInquiryChangedByRoom(rid); + } if (responses[2].modifiedCount) { void notifyOnSubscriptionChangedByRoomId(rid); } diff --git a/apps/meteor/app/livechat/server/lib/closeRoom.ts b/apps/meteor/app/livechat/server/lib/closeRoom.ts index 3ff105051a0..717679035ff 100644 --- a/apps/meteor/app/livechat/server/lib/closeRoom.ts +++ b/apps/meteor/app/livechat/server/lib/closeRoom.ts @@ -12,6 +12,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { client, shouldRetryTransaction } from '../../../../server/database/utils'; import { notifyOnLivechatInquiryChanged, + notifyOnRoomChanged, notifyOnRoomChangedById, notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; @@ -179,6 +180,9 @@ async function doCloseRoom( if (!params.forceClose && removedInquiry && removedInquiry.deletedCount !== 1) { throw new Error('Error removing inquiry'); } + if (removedInquiry.deletedCount) { + void notifyOnLivechatInquiryChanged(inquiry!, 'removed'); + } const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData, { session }); if (!params.forceClose && (!updatedRoom || updatedRoom.modifiedCount !== 1)) { @@ -207,6 +211,7 @@ async function doCloseRoom( throw new Error('Error: Room not found'); } + void notifyOnRoomChanged(newRoom, 'updated'); return { room: newRoom, closedBy: closeData.closedBy, removedInquiry: inquiry }; } diff --git a/apps/meteor/app/livechat/server/lib/rooms.ts b/apps/meteor/app/livechat/server/lib/rooms.ts index d5443812e3d..b52dc76b25f 100644 --- a/apps/meteor/app/livechat/server/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/lib/rooms.ts @@ -37,6 +37,7 @@ import { notifyOnRoomChangedById, notifyOnLivechatInquiryChanged, notifyOnSubscriptionChanged, + notifyOnRoomChanged, } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { i18n } from '../../../utils/lib/i18n'; @@ -278,6 +279,9 @@ export async function removeOmnichannelRoom(rid: string) { if (result[3]?.status === 'fulfilled' && result[3].value?.deletedCount && inquiry) { void notifyOnLivechatInquiryChanged(inquiry, 'removed'); } + if (result[4]?.status === 'fulfilled' && result[4].value?.deletedCount) { + void notifyOnRoomChanged(room, 'removed'); + } for (const r of result) { if (r.status === 'rejected') { diff --git a/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts b/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts index d772b0cb7f2..be0c765dd78 100644 --- a/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts +++ b/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts @@ -68,6 +68,7 @@ export const validators: OmnichannelRoomAccessValidator[] = [ ], }; + // TODO: findone filtering if the inquiry is queued instead of checking here const inquiry = await LivechatInquiry.findOne(filter, { projection: { status: 1 } }); return inquiry && inquiry.status === 'queued'; }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/priorities.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/priorities.ts index 09d9ab14212..fe6dd51c3b5 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/priorities.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/priorities.ts @@ -5,6 +5,7 @@ import type { PaginatedResult } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { FindOptions } from 'mongodb'; +import { notifyOnLivechatInquiryChangedByRoom, notifyOnRoomChanged } from '../../../../../../app/lib/server/lib/notifyListener'; import { logger } from '../../lib/logger'; type FindPriorityParams = { @@ -24,7 +25,7 @@ export async function findPriority({ ...(text && { $or: [{ name: new RegExp(escapeRegExp(text), 'i') }, { description: new RegExp(escapeRegExp(text), 'i') }] }), }; - const { cursor, totalCount } = await LivechatPriority.findPaginated(query, { + const { cursor, totalCount } = LivechatPriority.findPaginated(query, { sort: sort || { name: 1 }, skip: offset, limit: count, @@ -64,7 +65,7 @@ export const updateRoomPriority = async ( user: Required>, priorityId: string, ): Promise => { - const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1 } }); + const room = await LivechatRooms.findOneById(rid); if (!room) { throw new Error('error-room-does-not-exist'); } @@ -79,10 +80,15 @@ export const updateRoomPriority = async ( 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>): Promise => { - const room = await LivechatRooms.findOneById>(rid, { projection: { _id: 1 } }); + const room = await LivechatRooms.findOneById>(rid, { + projection: { priorityId: 0, priorityWeight: 0 }, + }); if (!room) { throw new Error('error-room-does-not-exist'); } @@ -92,6 +98,9 @@ export const removePriorityFromRoom = async (rid: string, user: Required { const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId }); const openRooms = await LivechatRooms.findOpenBySlaId(slaId, { projection: { _id: 1 } }, extraQuery).toArray(); + const openRoomIds: string[] = openRooms.map(({ _id }) => _id); if (openRooms.length) { - const openRoomIds: string[] = openRooms.map(({ _id }) => _id); await LivechatInquiry.bulkUnsetSla(openRoomIds); + void notifyOnLivechatInquiryChangedByRoom(openRoomIds, 'updated'); } await LivechatRooms.bulkRemoveSlaFromRoomsById(slaId); + void notifyOnRoomChangedById(openRoomIds, 'updated'); }; export const updateInquiryQueueSla = async (roomId: string, sla: Pick) => { @@ -29,6 +36,8 @@ export const updateInquiryQueueSla = async (roomId: string, sla: Pick) => { diff --git a/apps/meteor/server/services/omnichannel/queue.ts b/apps/meteor/server/services/omnichannel/queue.ts index 7e64b906799..e50c8f05a11 100644 --- a/apps/meteor/server/services/omnichannel/queue.ts +++ b/apps/meteor/server/services/omnichannel/queue.ts @@ -1,10 +1,11 @@ import { ServiceStarter } from '@rocket.chat/core-services'; -import { type InquiryWithAgentInfo, type IOmnichannelQueue } from '@rocket.chat/core-typings'; +import { LivechatInquiryStatus, type InquiryWithAgentInfo, type IOmnichannelQueue } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { LivechatInquiry, LivechatRooms } from '@rocket.chat/models'; import { tracerSpan } from '@rocket.chat/tracing'; import { queueLogger } from './logger'; +import { notifyOnLivechatInquiryChangedByRoom } from '../../../app/lib/server/lib/notifyListener'; import { getOmniChatSortQuery } from '../../../app/livechat/lib/inquiries'; import { dispatchAgentDelegated } from '../../../app/livechat/server/lib/Helper'; import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'; @@ -188,6 +189,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { step: 'reconciliation', }); await LivechatInquiry.removeByRoomId(roomId); + void notifyOnLivechatInquiryChangedByRoom(roomId, 'removed'); break; } case 'taken': { @@ -199,6 +201,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { }); // Reconciliate served inquiries, by updating their status to taken after queue tried to pick and failed await LivechatInquiry.takeInquiry(inquiryId); + void notifyOnLivechatInquiryChangedByRoom(roomId, 'updated', { status: LivechatInquiryStatus.TAKEN, takenAt: new Date() }); break; } case 'missing': { @@ -209,6 +212,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { step: 'reconciliation', }); await LivechatInquiry.removeByRoomId(roomId); + void notifyOnLivechatInquiryChangedByRoom(roomId, 'removed'); break; } default: { diff --git a/packages/model-typings/src/models/ILivechatInquiryModel.ts b/packages/model-typings/src/models/ILivechatInquiryModel.ts index 10c1bc76c8c..13fb344fd8e 100644 --- a/packages/model-typings/src/models/ILivechatInquiryModel.ts +++ b/packages/model-typings/src/models/ILivechatInquiryModel.ts @@ -36,7 +36,7 @@ export interface ILivechatInquiryModel extends IBaseModel; queueInquiryAndRemoveDefaultAgent(inquiryId: string): Promise; readyInquiry(inquiryId: string): Promise; - changeDepartmentIdByRoomId(rid: string, department: string): Promise; + changeDepartmentIdByRoomId(rid: string, department: string): Promise; getStatus(inquiryId: string): Promise; updateVisitorStatus(token: string, status: ILivechatInquiryRecord['v']['status']): Promise; setDefaultAgentById(inquiryId: string, defaultAgent: ILivechatInquiryRecord['defaultAgent']): Promise; diff --git a/packages/models/src/models/LivechatInquiry.ts b/packages/models/src/models/LivechatInquiry.ts index 918f66f8404..d3c9b18bc0b 100644 --- a/packages/models/src/models/LivechatInquiry.ts +++ b/packages/models/src/models/LivechatInquiry.ts @@ -373,7 +373,7 @@ export class LivechatInquiryRaw extends BaseRaw implemen ); } - async changeDepartmentIdByRoomId(rid: string, department: string): Promise { + async changeDepartmentIdByRoomId(rid: string, department: string): Promise { const query = { rid, }; @@ -383,7 +383,7 @@ export class LivechatInquiryRaw extends BaseRaw implemen }, }; - await this.updateOne(query, updateObj); + return this.updateOne(query, updateObj); } async getStatus(inquiryId: string): Promise {