Skip to content

Commit

Permalink
feat: Omnichannel MAC limitations (#30463)
Browse files Browse the repository at this point in the history
  • Loading branch information
KevLehman authored and hugocostadev committed Oct 26, 2023
1 parent b4ba639 commit bc5e088
Show file tree
Hide file tree
Showing 47 changed files with 451 additions and 61 deletions.
10 changes: 10 additions & 0 deletions .changeset/seven-emus-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-services": patch
"@rocket.chat/core-typings": patch
"@rocket.chat/rest-typings": patch
"@rocket.chat/ddp-client": patch
---

feat: Improve UI when MAC limits are reached
feat: Limit endpoints on MAC limit reached
6 changes: 3 additions & 3 deletions apps/meteor/app/lib/server/methods/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast<IMe
const room = await canSendMessageAsync(rid, { uid, username: user.username, type: user.type });

metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736
return sendMessage(user, message, room, false, previewUrls);
return await sendMessage(user, message, room, false, previewUrls);
} catch (err: any) {
SystemLogger.error({ msg: 'Error sending message:', err });

Expand All @@ -107,7 +107,7 @@ declare module '@rocket.chat/ui-contexts' {
}

Meteor.methods<ServerMethods>({
sendMessage(message, previewUrls) {
async sendMessage(message, previewUrls) {
check(message, Object);

const uid = Meteor.userId();
Expand All @@ -118,7 +118,7 @@ Meteor.methods<ServerMethods>({
}

try {
return executeSendMessage(uid, message, previewUrls);
return await executeSendMessage(uid, message, previewUrls);
} catch (error: any) {
if ((error.error || error.message) === 'error-not-allowed') {
throw new Meteor.Error(error.error || error.message, error.reason, {
Expand Down
9 changes: 9 additions & 0 deletions apps/meteor/app/livechat/server/api/v1/room.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Omnichannel } from '@rocket.chat/core-services';
import type { ILivechatAgent, IOmnichannelRoom, IUser, SelectedAgent, TransferByData } from '@rocket.chat/core-typings';
import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, Users, LivechatRooms, Subscriptions, Messages } from '@rocket.chat/models';
Expand Down Expand Up @@ -326,6 +327,10 @@ API.v1.addRoute(
throw new Error('This_conversation_is_already_closed');
}

if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Error('error-mac-limit-reached');
}

const guest = await LivechatVisitors.findOneEnabledById(room.v?._id);
if (!guest) {
throw new Error('error-invalid-visitor');
Expand Down Expand Up @@ -412,6 +417,10 @@ API.v1.addRoute(
throw new Error('error-invalid-room');
}

if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Error('error-mac-limit-reached');
}

if (!(await canAccessRoomAsync(room, user))) {
throw new Error('error-not-allowed');
}
Expand Down
9 changes: 7 additions & 2 deletions apps/meteor/app/livechat/server/api/v1/transcript.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Omnichannel } from '@rocket.chat/core-services';
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms, Users } from '@rocket.chat/models';
import { isPOSTLivechatTranscriptParams, isPOSTLivechatTranscriptRequestParams } from '@rocket.chat/rest-typings';
Expand Down Expand Up @@ -34,8 +35,8 @@ API.v1.addRoute(
{
async delete() {
const { rid } = this.urlParams;
const room = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, 'open' | 'transcriptRequest'>>(rid, {
projection: { open: 1, transcriptRequest: 1 },
const room = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, 'open' | 'transcriptRequest' | 'v'>>(rid, {
projection: { open: 1, transcriptRequest: 1, v: 1 },
});

if (!room?.open) {
Expand All @@ -45,6 +46,10 @@ API.v1.addRoute(
throw new Error('error-transcript-not-requested');
}

if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Error('error-mac-limit-reached');
}

await LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid);

return API.v1.success();
Expand Down
11 changes: 10 additions & 1 deletion apps/meteor/app/livechat/server/api/v1/videoCall.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Message } from '@rocket.chat/core-services';
import { Message, Omnichannel } from '@rocket.chat/core-services';
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { Messages, Settings, Rooms } from '@rocket.chat/models';
import { isGETWebRTCCall, isPUTWebRTCCallId } from '@rocket.chat/rest-typings';

Expand Down Expand Up @@ -27,6 +28,10 @@ API.v1.addRoute(
throw new Error('invalid-room');
}

if (!(await Omnichannel.isWithinMACLimit(room as IOmnichannelRoom))) {
throw new Error('error-mac-limit-reached');
}

const webrtcCallingAllowed = rcSettings.get('WebRTC_Enabled') === true && rcSettings.get('Omnichannel_call_provider') === 'WebRTC';
if (!webrtcCallingAllowed) {
throw new Error('webRTC calling not enabled');
Expand Down Expand Up @@ -79,6 +84,10 @@ API.v1.addRoute(
throw new Error('invalid-room');
}

if (!(await Omnichannel.isWithinMACLimit(room as IOmnichannelRoom))) {
throw new Error('error-mac-limit-reached');
}

const call = await Messages.findOneById(callId);
if (!call || call.t !== 'livechat_webrtc_video_call') {
throw new Error('invalid-callId');
Expand Down
30 changes: 30 additions & 0 deletions apps/meteor/app/livechat/server/hooks/checkMAC.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Omnichannel } from '@rocket.chat/core-services';
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { isEditedMessage } from '@rocket.chat/core-typings';

import { callbacks } from '../../../../lib/callbacks';

callbacks.add('beforeSaveMessage', async (message, room) => {
if (!room || room.t !== 'l') {
return message;
}

if (isEditedMessage(message)) {
return message;
}

if (message.token) {
return message;
}

if (message.t) {
return message;
}

const canSendMessage = await Omnichannel.isWithinMACLimit(room as IOmnichannelRoom);
if (!canSendMessage) {
throw new Error('error-mac-limit-reached');
}

return message;
});
1 change: 1 addition & 0 deletions apps/meteor/app/livechat/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import './hooks/saveContactLastChat';
import './hooks/saveLastMessageToInquiry';
import './hooks/afterUserActions';
import './hooks/afterAgentRemoved';
import './hooks/checkMAC';
import './methods/addAgent';
import './methods/addManager';
import './methods/changeLivechatStatus';
Expand Down
6 changes: 5 additions & 1 deletion apps/meteor/app/livechat/server/lib/Livechat.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Note: Please don't add any new methods to this file, since its still in js and we are migrating to ts
// Please add new methods to LivechatTyped.ts
import { Message } from '@rocket.chat/core-services';
import { Message, Omnichannel } from '@rocket.chat/core-services';
import { Logger } from '@rocket.chat/logger';
import {
LivechatVisitors,
Expand Down Expand Up @@ -411,6 +411,10 @@ export const Livechat = {
throw new Meteor.Error('error-transcript-already-requested', 'Transcript already requested');
}

if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Error('error-mac-limit-reached');
}

const { _id, username, name, utcOffset } = user;
const transcriptRequest = {
requestedAt: new Date(),
Expand Down
6 changes: 5 additions & 1 deletion apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import dns from 'dns';
import * as util from 'util';

import { Message, VideoConf, api } from '@rocket.chat/core-services';
import { Message, VideoConf, api, Omnichannel } from '@rocket.chat/core-services';
import type {
IOmnichannelRoom,
IOmnichannelRoomClosingInfo,
Expand Down Expand Up @@ -521,6 +521,10 @@ class LivechatClass {
throw new Error('error-invalid-room');
}

if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Error('error-mac-limit-reached');
}

const showAgentInfo = settings.get<string>('Livechat_show_agent_info');
const closingMessage = await Messages.findLivechatClosingMessage(rid, { projection: { ts: 1 } });
const ignoredMessageTypes: MessageTypesValues[] = [
Expand Down
9 changes: 9 additions & 0 deletions apps/meteor/app/livechat/server/lib/QueueManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Omnichannel } from '@rocket.chat/core-services';
import type { ILivechatInquiryRecord, ILivechatVisitor, IMessage, IOmnichannelRoom, SelectedAgent } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
Expand All @@ -20,6 +21,14 @@ export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent
logger.debug(`Delegating inquiry with id ${inquiry._id} to agent ${defaultAgent?.username}`);

await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent);
const room = await LivechatRooms.findOneById(inquiry.rid, { projection: { v: 1 } });
if (!room || !(await Omnichannel.isWithinMACLimit(room))) {
logger.error({ msg: 'MAC limit reached, not routing inquiry', inquiry });
// We'll queue these inquiries so when new license is applied, they just start rolling again
// Minimizing disruption
await saveQueueInquiry(inquiry);
return;
}
const dbInquiry = await LivechatInquiry.findOneById(inquiry._id);

if (!dbInquiry) {
Expand Down
14 changes: 14 additions & 0 deletions apps/meteor/app/livechat/server/lib/RoutingManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ export const RoutingManager: Routing = {
await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]);
}

if (!room) {
logger.debug(`Cannot assign agent to inquiry ${inquiry._id}: Room not found`);
throw new Meteor.Error('error-room-not-found', 'Room not found');
}

await dispatchAgentDelegated(rid, agent.agentId);
logger.debug(`Agent ${agent.agentId} assigned to inquriy ${inquiry._id}. Instances notified`);

Expand All @@ -173,6 +178,10 @@ export const RoutingManager: Routing = {
return false;
}

if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Error('error-mac-limit-reached');
}

if (departmentId && departmentId !== department) {
logger.debug(`Switching department for inquiry ${inquiry._id} [Current: ${department} | Next: ${departmentId}]`);
await updateChatDepartment({
Expand Down Expand Up @@ -260,6 +269,11 @@ export const RoutingManager: Routing = {
},

async transferRoom(room, guest, transferData) {
if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Error('error-mac-limit-reached');
}

logger.debug(`Transfering room ${room._id} by ${transferData.transferredBy._id}`);
if (transferData.departmentId) {
logger.debug(`Transfering room ${room._id} to department ${transferData.departmentId}`);
return forwardRoomToDepartment(room, guest, transferData);
Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/app/livechat/server/methods/returnAsInquiry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Omnichannel } from '@rocket.chat/core-services';
import type { ILivechatDepartment, IRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
Expand Down Expand Up @@ -29,6 +30,10 @@ Meteor.methods<ServerMethods>({
});
}

if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Meteor.Error('error-mac-limit-reached', 'MAC limit reached', { method: 'livechat:returnAsInquiry' });
}

if (!room.open) {
throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnAsInquiry' });
}
Expand Down
8 changes: 7 additions & 1 deletion apps/meteor/app/livechat/server/methods/takeInquiry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LivechatInquiry, Users } from '@rocket.chat/models';
import { Omnichannel } from '@rocket.chat/core-services';
import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';

Expand Down Expand Up @@ -48,6 +49,11 @@ export const takeInquiry = async (
});
}

const room = await LivechatRooms.findOneById(inquiry.rid);
if (!room || !(await Omnichannel.isWithinMACLimit(room))) {
throw new Error('error-mac-limit-reached');
}

const agent = {
agentId: user._id,
username: user.username,
Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/app/livechat/server/methods/transfer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Omnichannel } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import { LivechatVisitors, LivechatRooms, Subscriptions, Users } from '@rocket.chat/models';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
Expand Down Expand Up @@ -49,6 +50,10 @@ Meteor.methods<ServerMethods>({
throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:transfer' });
}

if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Meteor.Error('error-mac-limit-reached', 'MAC limit reached', { method: 'livechat:transfer' });
}

const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, uid, {
projection: { _id: 1 },
});
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/client/contexts/OmnichannelContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type OmnichannelContextValue = {
agentAvailable: boolean;
routeConfig?: OmichannelRoutingConfig;
showOmnichannelQueueLink: boolean;
isOverMacLimit: boolean;
livechatPriorities: {
data: Serialized<ILivechatPriority>[];
isLoading: boolean;
Expand All @@ -22,6 +23,7 @@ export const OmnichannelContext = createContext<OmnichannelContextValue>({
isEnterprise: false,
agentAvailable: false,
showOmnichannelQueueLink: false,
isOverMacLimit: false,
livechatPriorities: {
data: [],
isLoading: false,
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/client/hooks/omnichannel/useIsOverMacLimit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useOmnichannel } from './useOmnichannel';

export const useIsOverMacLimit = (): boolean => {
const { isOverMacLimit } = useOmnichannel();
return isOverMacLimit;
};
23 changes: 23 additions & 0 deletions apps/meteor/client/hooks/omnichannel/useIsRoomOverMacLimit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom, type IOmnichannelGenericRoom, isVoipRoom } from '@rocket.chat/core-typings';

import { useIsOverMacLimit } from './useIsOverMacLimit';

const getPeriod = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;

export const useIsRoomOverMacLimit = (room: IRoom) => {
const isOverMacLimit = useIsOverMacLimit();

if (!isOmnichannelRoom(room) && !isVoipRoom(room)) {
return false;
}

if (!room.open) {
return false;
}

const { v: { activity = [] } = {} } = room as IOmnichannelGenericRoom;

const currentPeriod = getPeriod(new Date());
return isOverMacLimit && !activity.includes(currentPeriod);
};
Loading

0 comments on commit bc5e088

Please sign in to comment.