Skip to content

Commit

Permalink
chore: add updater on afterOmnichannelSaveMessage (markRoomResponded)…
Browse files Browse the repository at this point in the history
… hook (#32983)

Co-authored-by: Guilherme Gazzo <[email protected]>
  • Loading branch information
ricardogarim and ggazzo authored Aug 7, 2024
1 parent c97b46e commit 5388eef
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 119 deletions.
104 changes: 52 additions & 52 deletions apps/meteor/app/livechat/server/hooks/markRoomResponded.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,74 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { isEditedMessage } from '@rocket.chat/core-typings';
import type { IOmnichannelRoom, IMessage } from '@rocket.chat/core-typings';
import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import { LivechatRooms, LivechatVisitors, LivechatInquiry } from '@rocket.chat/models';
import moment from 'moment';

import { callbacks } from '../../../../lib/callbacks';
import { notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener';

callbacks.add(
'afterOmnichannelSaveMessage',
async (message, { room }) => {
// skips this callback if the message was edited
if (!message || isEditedMessage(message)) {
return message;
}
export async function markRoomResponded(
message: IMessage,
room: IOmnichannelRoom,
roomUpdater: Updater<IOmnichannelRoom>,
): Promise<IOmnichannelRoom['responseBy'] | undefined> {
if (message.t || isEditedMessage(message) || isMessageFromVisitor(message)) {
return;
}

// skips this callback if the message is a system message
if (message.t) {
return message;
}
const monthYear = moment().format('YYYY-MM');
const isVisitorActive = await LivechatVisitors.isVisitorActiveOnPeriod(room.v._id, monthYear);

// if the message has a token, it was sent by the visitor, so ignore it
if (message.token) {
return message;
}
// Case: agent answers & visitor is not active, we mark visitor as active
if (!isVisitorActive) {
await LivechatVisitors.markVisitorActiveForPeriod(room.v._id, monthYear);
}

// Return YYYY-MM from moment
const monthYear = moment().format('YYYY-MM');
const isVisitorActive = await LivechatVisitors.isVisitorActiveOnPeriod(room.v._id, monthYear);
if (!room.v?.activity?.includes(monthYear)) {
const [, livechatInquiry] = await Promise.all([
LivechatRooms.markVisitorActiveForPeriod(room._id, monthYear),
LivechatInquiry.markInquiryActiveForPeriod(room._id, monthYear),
]);

// Case: agent answers & visitor is not active, we mark visitor as active
if (!isVisitorActive) {
await LivechatVisitors.markVisitorActiveForPeriod(room.v._id, monthYear);
if (livechatInquiry) {
void notifyOnLivechatInquiryChanged(livechatInquiry, 'updated', { v: livechatInquiry.v });
}
}

if (!room.v?.activity?.includes(monthYear)) {
const [, livechatInquiry] = await Promise.all([
LivechatRooms.markVisitorActiveForPeriod(room._id, monthYear),
LivechatInquiry.markInquiryActiveForPeriod(room._id, monthYear),
]);
if (livechatInquiry) {
void notifyOnLivechatInquiryChanged(livechatInquiry, 'updated', { v: livechatInquiry.v });
}
}
if (room.responseBy) {
LivechatRooms.getAgentLastMessageTsUpdateQuery(roomUpdater);
}

if (!room.waitingResponse) {
// case where agent sends second message or any subsequent message in a room before visitor responds to the first message
// in this case, we just need to update the lastMessageTs of the responseBy object
if (room.responseBy) {
await LivechatRooms.setAgentLastMessageTs(room._id);
LivechatRooms.getAgentLastMessageTsUpdateQuery(roomUpdater);
}

// check if room is yet awaiting for response from visitor
if (!room.waitingResponse) {
// case where agent sends second message or any subsequent message in a room before visitor responds to the first message
// in this case, we just need to update the lastMessageTs of the responseBy object
if (room.responseBy) {
await LivechatRooms.setAgentLastMessageTs(room._id);
}
return message;
}
return room.responseBy;
}

const responseBy: IOmnichannelRoom['responseBy'] = room.responseBy || {
_id: message.u._id,
username: message.u.username,
firstResponseTs: new Date(message.ts),
lastMessageTs: new Date(message.ts),
};

LivechatRooms.getResponseByRoomIdUpdateQuery(responseBy, roomUpdater);

// This is the first message from agent after visitor had last responded
const responseBy: IOmnichannelRoom['responseBy'] = room.responseBy || {
_id: message.u._id,
username: message.u.username,
firstResponseTs: new Date(message.ts),
lastMessageTs: new Date(message.ts),
};
return responseBy;
}

// this unsets waitingResponse and sets responseBy object
await LivechatRooms.setResponseByRoomId(room._id, responseBy);
callbacks.add(
'afterOmnichannelSaveMessage',
async (message, { room, roomUpdater }) => {
if (!message || message.t || isEditedMessage(message) || isMessageFromVisitor(message)) {
return;
}

return message;
await markRoomResponded(message, room, roomUpdater);
},
callbacks.priority.HIGH,
'markRoomResponded',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,46 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { isEditedMessage } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';
import type { IMessage } from '@rocket.chat/core-typings';
import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings';
import moment from 'moment';

import { markRoomResponded } from '../../../../../app/livechat/server/hooks/markRoomResponded';
import { settings } from '../../../../../app/settings/server';
import { callbacks } from '../../../../../lib/callbacks';
import { setPredictedVisitorAbandonmentTime } from '../lib/Helper';

function shouldSaveInactivity(message: IMessage): boolean {
if (message.t || isEditedMessage(message) || isMessageFromVisitor(message)) {
return false;
}

const abandonedRoomsAction = settings.get('Livechat_abandoned_rooms_action');
const visitorInactivityTimeout = settings.get<number>('Livechat_visitor_inactivity_timeout');

if (!abandonedRoomsAction || abandonedRoomsAction === 'none' || visitorInactivityTimeout <= 0) {
return false;
}

return true;
}

callbacks.remove('afterOmnichannelSaveMessage', 'markRoomResponded');

callbacks.add(
'afterOmnichannelSaveMessage',
async (message, { room }) => {
if (
!settings.get('Livechat_abandoned_rooms_action') ||
settings.get('Livechat_abandoned_rooms_action') === 'none' ||
settings.get<number>('Livechat_visitor_inactivity_timeout') <= 0
) {
return message;
}
// skips this callback if the message was edited
if (isEditedMessage(message)) {
return message;
}
// if the message has a type means it is a special message (like the closing comment), so skip it
if (message.t) {
return message;
}
// message from visitor
if (message.token) {
return message;
}

const latestRoom = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, '_id' | 'responseBy' | 'departmentId'>>(room._id, {
projection: {
_id: 1,
responseBy: 1,
departmentId: 1,
},
});
async (message, { room, roomUpdater }) => {
const responseBy = await markRoomResponded(message, room, roomUpdater);

if (!latestRoom?.responseBy) {
if (!shouldSaveInactivity(message)) {
return message;
}

if (moment(latestRoom.responseBy.firstResponseTs).isSame(moment(message.ts))) {
await setPredictedVisitorAbandonmentTime(latestRoom);
if (!responseBy) {
return;
}

return message;
if (moment(responseBy.firstResponseTs).isSame(moment(message.ts))) {
await setPredictedVisitorAbandonmentTime({ ...room, responseBy }, roomUpdater);
}
},
callbacks.priority.MEDIUM,
'save-visitor-inactivity',
); // This hook priority should always be less than the priority of hook "markRoomResponded" bcs, the room.responseBy.firstMessage property set there is being used here for determining visitor abandonment
);
12 changes: 10 additions & 2 deletions apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { api } from '@rocket.chat/core-services';
import type { IOmnichannelRoom, IOmnichannelServiceLevelAgreements, InquiryWithAgentInfo } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import {
Rooms as RoomRaw,
LivechatRooms,
Expand Down Expand Up @@ -139,7 +140,10 @@ const dispatchWaitingQueueStatus = async (department?: string) => {
// but we don't need to notify _each_ change that takes place, just their final position
export const debouncedDispatchWaitingQueueStatus = memoizeDebounce(dispatchWaitingQueueStatus, 1200);

export const setPredictedVisitorAbandonmentTime = async (room: Pick<IOmnichannelRoom, '_id' | 'responseBy' | 'departmentId'>) => {
export const setPredictedVisitorAbandonmentTime = async (
room: Pick<IOmnichannelRoom, '_id' | 'responseBy' | 'departmentId'>,
roomUpdater?: Updater<IOmnichannelRoom>,
) => {
if (
!room.responseBy?.firstResponseTs ||
!settings.get('Livechat_abandoned_rooms_action') ||
Expand All @@ -160,7 +164,11 @@ export const setPredictedVisitorAbandonmentTime = async (room: Pick<IOmnichannel
}

const willBeAbandonedAt = moment(room.responseBy.firstResponseTs).add(Number(secondsToAdd), 'seconds').toDate();
await LivechatRooms.setPredictedVisitorAbandonmentByRoomId(room._id, willBeAbandonedAt);
if (roomUpdater) {
await LivechatRooms.getPredictedVisitorAbandonmentByRoomIdUpdateQuery(willBeAbandonedAt, roomUpdater);
} else {
await LivechatRooms.setPredictedVisitorAbandonmentByRoomId(room._id, willBeAbandonedAt);
}
};

export const updatePredictedVisitorAbandonment = async () => {
Expand Down
9 changes: 9 additions & 0 deletions apps/meteor/ee/server/models/raw/LivechatRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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 { Updater } from '@rocket.chat/models';
import type { FindCursor, UpdateResult, Document, FindOptions, Db, Collection, Filter, AggregationCursor } from 'mongodb';

import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred';
Expand All @@ -20,6 +21,7 @@ declare module '@rocket.chat/model-typings' {
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>;
Expand Down Expand Up @@ -209,6 +211,13 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo
);
}

getPredictedVisitorAbandonmentByRoomIdUpdateQuery(
date: Date,
roomUpdater: Updater<IOmnichannelRoom> = this.getUpdater(),
): Updater<IOmnichannelRoom> {
return roomUpdater.set('omnichannel.predictedVisitorAbandonmentAt', date);
}

setPredictedVisitorAbandonmentByRoomId(rid: string, willBeAbandonedAt: Date): Promise<UpdateResult> {
const query = {
_id: rid,
Expand Down
40 changes: 13 additions & 27 deletions apps/meteor/server/models/raw/LivechatRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1977,21 +1977,10 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
return this.find(query, options);
}

setResponseByRoomId(roomId: string, responseBy: IOmnichannelRoom['responseBy']) {
return this.updateOne(
{
_id: roomId,
t: 'l',
},
{
$set: {
responseBy,
},
$unset: {
waitingResponse: 1,
},
},
);
getResponseByRoomIdUpdateQuery(responseBy: IOmnichannelRoom['responseBy'], updater: Updater<IOmnichannelRoom> = this.getUpdater()) {
updater.set('responseBy', responseBy);
updater.unset('waitingResponse');
return updater;
}

setNotResponseByRoomId(roomId: string) {
Expand All @@ -2011,18 +2000,8 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
);
}

setAgentLastMessageTs(roomId: string) {
return this.updateOne(
{
_id: roomId,
t: 'l',
},
{
$set: {
'responseBy.lastMessageTs': new Date(),
},
},
);
getAgentLastMessageTsUpdateQuery(updater: Updater<IOmnichannelRoom> = this.getUpdater()) {
return updater.set('responseBy.lastMessageTs', new Date());
}

private getAnalyticsUpdateQuery(
Expand Down Expand Up @@ -2637,6 +2616,13 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
throw new Error('Method not implemented.');
}

getPredictedVisitorAbandonmentByRoomIdUpdateQuery(
_willBeAbandonedAt: Date,
_updater: Updater<IOmnichannelRoom>,
): Updater<IOmnichannelRoom> {
throw new Error('Method not implemented.');
}

setPredictedVisitorAbandonmentByRoomId(_rid: string, _willBeAbandonedAt: Date): Promise<UpdateResult> {
throw new Error('Method not implemented.');
}
Expand Down
7 changes: 5 additions & 2 deletions packages/model-typings/src/models/ILivechatRoomsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,12 @@ export interface ILivechatRoomsModel extends IBaseModel<IOmnichannelRoom> {
options?: FindOptions<IOmnichannelRoom>,
extraQuery?: Filter<IOmnichannelRoom>,
): FindCursor<IOmnichannelRoom>;
setResponseByRoomId(roomId: string, responseBy: IOmnichannelRoom['responseBy']): Promise<UpdateResult>;
getResponseByRoomIdUpdateQuery(
responseBy: IOmnichannelRoom['responseBy'],
updater?: Updater<IOmnichannelRoom>,
): Updater<IOmnichannelRoom>;
setNotResponseByRoomId(roomId: string): Promise<UpdateResult>;
setAgentLastMessageTs(roomId: string): Promise<UpdateResult>;
getAgentLastMessageTsUpdateQuery(updater?: Updater<IOmnichannelRoom>): Updater<IOmnichannelRoom>;
getAnalyticsUpdateQueryByRoomId(
room: IOmnichannelRoom,
message: IMessage,
Expand Down

0 comments on commit 5388eef

Please sign in to comment.