Skip to content

Commit

Permalink
refactor: omnichannel on-hold feature (#28252)
Browse files Browse the repository at this point in the history
  • Loading branch information
murtaza98 authored Jun 15, 2023
1 parent 207a191 commit 9da856c
Show file tree
Hide file tree
Showing 31 changed files with 636 additions and 196 deletions.
9 changes: 9 additions & 0 deletions .changeset/moody-teachers-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-services": patch
"@rocket.chat/core-typings": patch
"@rocket.chat/model-typings": patch
"@rocket.chat/rest-typings": patch
---

fix: Resume on-hold chat not working with max-chat's allowed per agent config
3 changes: 1 addition & 2 deletions apps/meteor/app/livechat/server/lib/Helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,6 @@ export const forwardRoomToAgent = async (room, transferData) => {
if (oldServedBy && servedBy._id !== oldServedBy._id) {
await RoutingManager.removeAllRoomSubscriptions(room, servedBy);
}
await Message.saveSystemMessage('uj', rid, servedBy.username, servedBy);

setImmediate(() => {
Apps.triggerEvent(AppEvents.IPostLivechatRoomTransferred, {
Expand Down Expand Up @@ -505,7 +504,7 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => {
await LivechatRooms.removeAgentByRoomId(rid);
await dispatchAgentDelegated(rid, null);
const newInquiry = await LivechatInquiry.findOneById(inquiry._id);
await queueInquiry(room, newInquiry);
await queueInquiry(newInquiry);

logger.debug(`Inquiry ${inquiry._id} queued succesfully`);
}
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/app/livechat/server/lib/QueueManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const saveQueueInquiry = async (inquiry) => {
await callbacks.run('livechat.afterInquiryQueued', inquiry);
};

export const queueInquiry = async (room, inquiry, defaultAgent) => {
export const queueInquiry = async (inquiry, defaultAgent) => {
const inquiryAgent = await RoutingManager.delegateAgent(defaultAgent, inquiry);
logger.debug(`Delegating inquiry with id ${inquiry._id} to agent ${defaultAgent?.username}`);

Expand Down Expand Up @@ -70,7 +70,7 @@ export const QueueManager = {

await LivechatRooms.updateRoomCount();

await queueInquiry(room, inquiry, agent);
await queueInquiry(inquiry, agent);
logger.debug(`Inquiry ${inquiry._id} queued`);

const newRoom = await LivechatRooms.findOneById(rid);
Expand Down Expand Up @@ -126,7 +126,7 @@ export const QueueManager = {
const inquiry = await LivechatInquiry.findOneById(await createLivechatInquiry({ rid, name, guest, message, extraData: { source } }));
logger.debug(`Generated inquiry for visitor ${v._id} with id ${inquiry._id} [Not queued]`);

await queueInquiry(room, inquiry, defaultAgent);
await queueInquiry(inquiry, defaultAgent);
logger.debug(`Inquiry ${inquiry._id} queued`);

return room;
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/app/livechat/server/lib/RoutingManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const RoutingManager = {
const user = await Users.findOneById(agent.agentId);
const room = await LivechatRooms.findOneById(rid);

await Message.saveSystemMessage('command', rid, 'connected', user);
await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]);

await dispatchAgentDelegated(rid, agent.agentId);
logger.debug(`Agent ${agent.agentId} assigned to inquriy ${inquiry._id}. Instances notified`);
Expand Down Expand Up @@ -176,7 +176,7 @@ export const RoutingManager = {
return room;
}

if (room.servedBy && room.servedBy._id === agent.agentId && !room.onHold) {
if (room.servedBy && room.servedBy._id === agent.agentId) {
logger.debug(`Cannot take Inquiry ${inquiry._id}: Already taken by agent ${room.servedBy._id}`);
return room;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useMethod } from '@rocket.chat/ui-contexts';
import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';

export const useResumeChatOnHoldMutation = (
options?: Omit<UseMutationOptions<void, Error, IRoom['_id']>, 'mutationFn'>,
): UseMutationResult<void, Error, IRoom['_id']> => {
const resumeChatOnHold = useMethod('livechat:resumeOnHold');
const resumeChatOnHold = useEndpoint('POST', '/v1/livechat/room.resumeOnHold');

const dispatchToastMessage = useToastMessageDispatch();

const queryClient = useQueryClient();

return useMutation(
async (rid) => {
await resumeChatOnHold(rid, { clientAction: true });
async (roomId) => {
await resumeChatOnHold({ roomId });
},
{
...options,
Expand All @@ -22,6 +24,9 @@ export const useResumeChatOnHoldMutation = (
await queryClient.invalidateQueries(['subscriptions', { rid }]);
return options?.onSuccess?.(data, rid, context);
},
onError: (error) => {
dispatchToastMessage({ type: 'error', message: error });
},
},
);
};
69 changes: 45 additions & 24 deletions apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,77 @@
import { Meteor } from 'meteor/meteor';
import { isPOSTLivechatRoomPriorityParams } from '@rocket.chat/rest-typings';
import { isLivechatRoomOnHoldProps, isLivechatRoomResumeOnHoldProps, isPOSTLivechatRoomPriorityParams } from '@rocket.chat/rest-typings';
import { LivechatRooms, Subscriptions } from '@rocket.chat/models';
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { OmnichannelEEService } from '@rocket.chat/core-services';

import { API } from '../../../../../app/api/server';
import { hasPermissionAsync } from '../../../../../app/authorization/server/functions/hasPermission';
import { LivechatEnterprise } from '../lib/LivechatEnterprise';
import { removePriorityFromRoom, updateRoomPriority } from './lib/priorities';
import { i18n } from '../../../../../server/lib/i18n';

API.v1.addRoute(
'livechat/room.onHold',
{ authRequired: true, permissionsRequired: ['on-hold-livechat-room'] },
{ authRequired: true, permissionsRequired: ['on-hold-livechat-room'], validateParams: isLivechatRoomOnHoldProps },
{
async post() {
const { roomId } = this.bodyParams;
if (!roomId || roomId.trim() === '') {
return API.v1.failure('Invalid room Id');
}

const room = await LivechatRooms.findOneById(roomId);
if (!room || room.t !== 'l') {
return API.v1.failure('Invalid room Id');
}
type Room = Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold' | 'lastMessage' | 'servedBy'>;

if (room.lastMessage?.token) {
return API.v1.failure('You cannot place chat on-hold, when the Contact has sent the last message');
const room = await LivechatRooms.findOneById<Room>(roomId, {
projection: { _id: 1, t: 1, open: 1, onHold: 1, lastMessage: 1, servedBy: 1 },
});
if (!room) {
throw new Error('error-invalid-room');
}

if (room.onHold) {
return API.v1.failure('Room is already On-Hold');
const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId, { projection: { _id: 1 } });
if (!subscription && !(await hasPermissionAsync(this.userId, 'on-hold-others-livechat-room'))) {
throw new Error('Not_authorized');
}

if (!room.open) {
return API.v1.failure('Room cannot be placed on hold after being closed');
const onHoldBy = { _id: this.userId, username: this.user.username, name: this.user.name };
const comment = i18n.t('Omnichannel_On_Hold_manually', {
user: onHoldBy.name || `@${onHoldBy.username}`,
});

await OmnichannelEEService.placeRoomOnHold(room, comment, this.user);

return API.v1.success();
},
},
);

API.v1.addRoute(
'livechat/room.resumeOnHold',
{ authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLivechatRoomResumeOnHoldProps },
{
async post() {
const { roomId } = this.bodyParams;
if (!roomId || roomId.trim() === '') {
throw new Error('invalid-param');
}

const user = await Meteor.userAsync();
if (!user) {
return API.v1.failure('Invalid user');
type Room = Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold' | 'servedBy'>;

const room = await LivechatRooms.findOneById<Room>(roomId, {
projection: { t: 1, open: 1, onHold: 1, servedBy: 1 },
});
if (!room) {
throw new Error('error-invalid-room');
}

const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { projection: { _id: 1 } });
const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId, { projection: { _id: 1 } });
if (!subscription && !(await hasPermissionAsync(this.userId, 'on-hold-others-livechat-room'))) {
return API.v1.failure('Not authorized');
throw new Error('Not_authorized');
}

const onHoldBy = { _id: user._id, username: user.username, name: (user as any).name };
const { name, username, _id: userId } = this.user;
const onHoldBy = { _id: userId, username, name };
const comment = i18n.t('Omnichannel_On_Hold_manually', {
user: onHoldBy.name || `@${onHoldBy.username}`,
});

await LivechatEnterprise.placeRoomOnHold(room, comment, onHoldBy);
await OmnichannelEEService.resumeRoomOnHold(room, comment, this.user, true);

return API.v1.success();
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';

import { callbacks } from '../../../../../lib/callbacks';
import { settings } from '../../../../../app/settings/server';
import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler';
Expand All @@ -6,7 +8,7 @@ import { i18n } from '../../../../../server/lib/i18n';

let autoCloseOnHoldChatTimeout = 0;

const handleAfterOnHold = async (room: any = {}): Promise<any> => {
const handleAfterOnHold = async (room: Pick<IOmnichannelRoom, '_id'>): Promise<any> => {
const { _id: rid } = room;
if (!rid) {
cbLogger.debug('Skipping callback. No room provided');
Expand All @@ -24,7 +26,7 @@ const handleAfterOnHold = async (room: any = {}): Promise<any> => {
i18n.t('Closed_automatically_because_chat_was_onhold_for_seconds', {
onHoldTime: autoCloseOnHoldChatTimeout,
});
await AutoCloseOnHoldScheduler.scheduleRoom(room._id, autoCloseOnHoldChatTimeout, closeComment);
await AutoCloseOnHoldScheduler.scheduleRoom(rid, autoCloseOnHoldChatTimeout, closeComment);
};

settings.watch<number>('Livechat_auto_close_on_hold_chats_timeout', (value) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';

import { callbacks } from '../../../../../lib/callbacks';
import { LivechatEnterprise } from '../lib/LivechatEnterprise';
import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler';
import { cbLogger } from '../lib/logger';

const handleAfterOnHoldChatResumed = async (room: any): Promise<void> => {
if (!room?._id || !room.onHold) {
cbLogger.debug('Skipping callback. No room provided or room is not on hold');
return;
type IRoom = Pick<IOmnichannelRoom, '_id'>;

const handleAfterOnHoldChatResumed = async (room: IRoom): Promise<IRoom> => {
if (!room?._id) {
cbLogger.debug('Skipping callback. No room provided');
return room;
}

cbLogger.debug(`Removing current on hold timers for room ${room._id}`);
void LivechatEnterprise.releaseOnHoldChat(room);
const { _id: roomId } = room;

cbLogger.debug(`Removing current on hold timers for room ${roomId}`);
await AutoCloseOnHoldScheduler.unscheduleRoom(roomId);

return room;
};

callbacks.add(
'livechat:afterOnHoldChatResumed',
(room) => handleAfterOnHoldChatResumed(room),
handleAfterOnHoldChatResumed,
callbacks.priority.HIGH,
'livechat-after-on-hold-chat-resumed',
);
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Subscriptions, LivechatInquiry, LivechatRooms } from '@rocket.chat/models';

import { callbacks } from '../../../../../lib/callbacks';
import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager';
import { settings } from '../../../../../app/settings/server';
import { cbLogger } from '../lib/logger';
import { dispatchAgentDelegated } from '../../../../../app/livechat/server/lib/Helper';

const handleOnAgentAssignmentFailed = async ({
inquiry,
Expand All @@ -23,24 +19,6 @@ const handleOnAgentAssignmentFailed = async ({
return;
}

if (room.onHold) {
cbLogger.debug('Room is on hold. Removing current assignations before queueing again');
const { _id: roomId } = room;

const { _id: inquiryId } = inquiry;
await LivechatInquiry.queueInquiryAndRemoveDefaultAgent(inquiryId);
await LivechatRooms.removeAgentByRoomId(roomId);
await Subscriptions.removeByRoomId(roomId);
await dispatchAgentDelegated(roomId, null);

const newInquiry = await LivechatInquiry.findOneById(inquiryId);

await queueInquiry(room, newInquiry);

cbLogger.debug('Room queued successfully');
return;
}

if (!settings.get('Livechat_waiting_queue')) {
cbLogger.debug('Skipping callback. Queue disabled by setting');
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms, Subscriptions } from '@rocket.chat/models';

import { callbacks } from '../../../../../lib/callbacks';
import { settings } from '../../../../../app/settings/server';
import { debouncedDispatchWaitingQueueStatus } from '../lib/Helper';
import { LivechatEnterprise } from '../lib/LivechatEnterprise';
import { callbackLogger } from '../../../../../app/livechat/server/lib/callbackLogger';
import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler';

callbacks.add(
'livechat.closeRoom',
async (params) => {
const { room } = params;
type LivechatCloseCallbackParams = {
room: IOmnichannelRoom;
};

await LivechatEnterprise.releaseOnHoldChat(room);
const onCloseLivechat = async (params: LivechatCloseCallbackParams) => {
const {
room,
room: { _id: roomId },
} = params;

if (!settings.get('Livechat_waiting_queue')) {
return params;
}
callbackLogger.debug(`[onCloseLivechat] clearing onHold related data for room ${roomId}`);

const { departmentId } = room || {};
debouncedDispatchWaitingQueueStatus(departmentId);
await Promise.all([
LivechatRooms.unsetOnHoldByRoomId(roomId),
Subscriptions.unsetOnHoldByRoomId(roomId),
AutoCloseOnHoldScheduler.unscheduleRoom(roomId),
]);

callbackLogger.debug(`[onCloseLivechat] clearing onHold related data for room ${roomId} completed`);

if (!settings.get('Livechat_waiting_queue')) {
return params;
},
}

const { departmentId } = room || {};
callbackLogger.debug(`[onCloseLivechat] dispatching waiting queue status for department ${departmentId}`);
debouncedDispatchWaitingQueueStatus(departmentId);

return params;
};

callbacks.add(
'livechat.closeRoom',
(params: LivechatCloseCallbackParams) => onCloseLivechat(params),
callbacks.priority.HIGH,
'livechat-waiting-queue-monitor-close-room',
);
Loading

0 comments on commit 9da856c

Please sign in to comment.