Skip to content

Commit

Permalink
Chore: Move voip's Wrap-up and On-hold functionality to EE (Backend) (#…
Browse files Browse the repository at this point in the history
…25160)

<!-- This is a pull request template, you do not need to uncomment or remove the comments, they won't show up in the PR text. -->

<!-- Your Pull Request name should start with one of the following tags
  [NEW] For new features
  [IMPROVE] For an improvement (performance or little improvements) in existing features
  [FIX] For bug fixes that affect the end-user
  [BREAK] For pull requests including breaking changes
  Chore: For small tasks
  Doc: For documentation
-->

<!-- Checklist!!! If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code. 
  - I have read the Contributing Guide - https://github.com/RocketChat/Rocket.Chat/blob/develop/.github/CONTRIBUTING.md#contributing-to-rocketchat doc
  - I have signed the CLA - https://cla-assistant.io/RocketChat/Rocket.Chat
  - Lint and unit tests pass locally with my changes
  - I have added tests that prove my fix is effective or that my feature works (if applicable)
  - I have added necessary documentation (if applicable)
  - Any dependent changes have been merged and published in downstream modules
-->

## Proposed changes (including videos or screenshots)
<!-- CHANGELOG -->
<!--
  Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
  If it fixes a bug or resolves a feature request, be sure to link to that issue below.
  This description will appear in the release notes if we accept the contribution.
-->

<!-- END CHANGELOG -->

## Issue(s)
<!-- Link the issues being closed by or related to this PR. For example, you can use #594 if this PR closes issue number 594 -->

## Steps to test or reproduce
<!-- Mention how you would reproduce the bug if not mentioned on the issue page already. Also mention which screens are going to have the changes if applicable -->

## Further comments
<!-- If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->
  • Loading branch information
murtaza98 committed Jun 9, 2022
1 parent 9f2c03c commit a741aec
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 88 deletions.
13 changes: 4 additions & 9 deletions apps/meteor/app/api/server/v1/voip/rooms.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Match, check } from 'meteor/check';
import { Random } from 'meteor/random';
import type { ILivechatAgent } from '@rocket.chat/core-typings';
import { isVoipRoomCloseProps } from '@rocket.chat/rest-typings/dist/v1/voip';

import { API } from '../../api';
import { VoipRoom, LivechatVisitors, Users } from '../../../../models/server/raw';
Expand Down Expand Up @@ -216,16 +217,10 @@ API.v1.addRoute(
*/
API.v1.addRoute(
'voip/room.close',
{ authRequired: true, permissionsRequired: ['inbound-voip-calls'] },
{ authRequired: true, validateParams: isVoipRoomCloseProps, permissionsRequired: ['inbound-voip-calls'] },
{
async post() {
check(this.bodyParams, {
rid: String,
token: String,
comment: Match.Maybe(String),
tags: Match.Maybe([String]),
});
const { rid, token, comment, tags } = this.bodyParams;
const { rid, token, options } = this.bodyParams;

const visitor = await LivechatVisitors.getVisitorByToken(token, {});
if (!visitor) {
Expand All @@ -238,7 +233,7 @@ API.v1.addRoute(
if (!room.open) {
return API.v1.failure('room-closed');
}
const closeResult = await LivechatVoip.closeRoom(visitor, room, this.user, comment, tags);
const closeResult = await LivechatVoip.closeRoom(visitor, room, this.user, 'voip-call-wrapup', options);
if (!closeResult) {
return API.v1.failure();
}
Expand Down
10 changes: 2 additions & 8 deletions apps/meteor/client/providers/CallProvider/CallProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,8 @@ export const CallProvider: FC = ({ children }) => {
}
return '';
},
closeRoom: async (data?: { comment: string; tags: string[] }): Promise<void> => {
roomInfo &&
(await voipCloseRoomEndpoint({
rid: roomInfo.rid,
token: roomInfo.v.token || '',
comment: data?.comment || '',
tags: data?.tags,
}));
closeRoom: async ({ comment, tags }: { comment?: string; tags?: string[] }): Promise<void> => {
roomInfo && (await voipCloseRoomEndpoint({ rid: roomInfo.rid, token: roomInfo.v.token || '', options: { comment, tags } }));
homeRoute.push({});
const queueAggregator = voipClient.getAggregator();
if (queueAggregator) {
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/ee/app/voip-enterprise/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './services/voipService';
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { IVoipRoom } from '@rocket.chat/core-typings';

import { PbxEvent } from '../../../../../app/models/server/raw';

export const calculateOnHoldTimeForRoom = async (room: IVoipRoom, closedAt: Date): Promise<number> => {
if (!room.callUniqueId) {
return 0;
}

const events = await PbxEvent.findByEvents(room.callUniqueId, ['Hold', 'Unhold']).toArray();
if (!events.length) {
// if there's no events, that means no hold time
return 0;
}

if (events.length === 1 && events[0].event === 'Unhold') {
// if the only event is an unhold event, something bad happened
return 0;
}

if (events.length === 1 && events[0].event === 'Hold') {
// if the only event is a hold event, the call was ended while on hold
// hold time = room.closedAt - event.ts
return closedAt.getTime() - events[0].ts.getTime();
}

let currentOnHoldTime = 0;

for (let i = 0; i < events.length; i += 2) {
const onHold = events[i].ts;
const unHold = events[i + 1]?.ts || closedAt;

currentOnHoldTime += unHold.getTime() - onHold.getTime();
}

return currentOnHoldTime;
};
41 changes: 41 additions & 0 deletions apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ILivechatAgent, ILivechatVisitor, IRoomClosingInfo, IUser, IVoipRoom } from '@rocket.chat/core-typings';

import { IOmniRoomClosingMessage } from '../../../../../server/services/omnichannel-voip/internalTypes';
import { OmnichannelVoipService } from '../../../../../server/services/omnichannel-voip/service';
import { overwriteClassOnLicense } from '../../../license/server';
import { calculateOnHoldTimeForRoom } from '../lib/calculateOnHoldTimeForRoom';

overwriteClassOnLicense('livechat-enterprise', OmnichannelVoipService, {
getRoomClosingData(
_originalFn: (
closer: ILivechatVisitor | ILivechatAgent,
room: IVoipRoom,
user: IUser,
sysMessageId?: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly',
options?: { comment?: string | null; tags?: string[] | null },
) => Promise<boolean>,
closeInfo: IRoomClosingInfo,
closeSystemMsgData: IOmniRoomClosingMessage,
room: IVoipRoom,
sysMessageId: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly',
options?: { comment?: string; tags?: string[] },
): { closeInfo: IRoomClosingInfo; closeSystemMsgData: IOmniRoomClosingMessage } {
const { comment, tags } = options || {};
if (comment) {
closeSystemMsgData.msg = comment;
}
if (tags?.length) {
closeInfo.tags = tags;
}

if (sysMessageId === 'voip-call-wrapup' && !comment) {
closeSystemMsgData.t = 'voip-call-ended';
}

const now = new Date();
const callTotalHoldTime = Promise.await(calculateOnHoldTimeForRoom(room, now));
closeInfo.callTotalHoldTime = callTotalHoldTime;

return { closeInfo, closeSystemMsgData };
},
});
1 change: 1 addition & 0 deletions apps/meteor/ee/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '../app/auditing/server/index';
import '../app/authorization/server/index';
import '../app/canned-responses/server/index';
import '../app/livechat-enterprise/server/index';
import '../app/voip-enterprise/server/index';
import '../app/settings/server/index';
import '../app/teams-mention/server/index';
import './api';
Expand Down
8 changes: 7 additions & 1 deletion apps/meteor/server/sdk/types/IOmnichannelVoipService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ export interface IOmnichannelVoipService {
options: FindOneOptions<IVoipRoom>,
): Promise<IRoomCreationResponse>;
findRoom(token: string, rid: string): Promise<IVoipRoom | null>;
closeRoom(closer: ILivechatVisitor | ILivechatAgent, room: IVoipRoom, user: IUser, comment?: string, tags?: string[]): Promise<boolean>;
closeRoom(
closer: ILivechatVisitor | ILivechatAgent,
room: IVoipRoom,
user: IUser,
sysMessageId?: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly',
options?: { comment?: string | null; tags?: string[] | null },
): Promise<boolean>;
handleEvent(
event: VoipClientEvents,
room: IRoom,
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/server/services/omnichannel-voip/internalTypes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IMessage } from '@rocket.chat/core-typings';

export type FindVoipRoomsParams = {
agents?: string[];
open?: boolean;
Expand All @@ -13,3 +15,5 @@ export type FindVoipRoomsParams = {
offset?: number;
};
};

export type IOmniRoomClosingMessage = Pick<IMessage, 't' | 'groupable'> & Partial<Pick<IMessage, 'msg'>>;
108 changes: 48 additions & 60 deletions apps/meteor/server/services/omnichannel-voip/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { UsersRaw } from '../../../app/models/server/raw/Users';
import { VoipRoomsRaw } from '../../../app/models/server/raw/VoipRooms';
import { PbxEventsRaw } from '../../../app/models/server/raw/PbxEvents';
import { sendMessage } from '../../../app/lib/server/functions/sendMessage';
import { FindVoipRoomsParams } from './internalTypes';
import { FindVoipRoomsParams, IOmniRoomClosingMessage } from './internalTypes';
import { api } from '../../sdk/api';

export class OmnichannelVoipService extends ServiceClassInternal implements IOmnichannelVoipService {
Expand Down Expand Up @@ -99,7 +99,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
// and multiple rooms are left opened for one single agent. Best case this will iterate once
for await (const room of openRooms) {
await this.handleEvent(VoipClientEvents['VOIP-CALL-ENDED'], room, agent, 'Agent disconnected abruptly');
await this.closeRoom(agent, room, agent, 'Agent disconnected abruptly', undefined, 'voip-call-ended-unexpectedly');
await this.closeRoom(agent, room, agent, 'voip-call-ended-unexpectedly', { comment: 'Agent disconnected abruptly' });
}
}

Expand Down Expand Up @@ -266,87 +266,75 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
return this.voipRoom.findOneByIdAndVisitorToken(rid, token, { projection });
}

private async calculateOnHoldTimeForRoom(room: IVoipRoom, closedAt: Date): Promise<number> {
if (!room || !room.callUniqueId) {
return 0;
}

const events = await this.pbxEvents.findByEvents(room.callUniqueId, ['Hold', 'Unhold']).toArray();
if (!events.length) {
// if there's no events, that means no hold time
return 0;
}

if (events.length === 1 && events[0].event === 'Unhold') {
// if the only event is an unhold event, something bad happened
return 0;
}

if (events.length === 1 && events[0].event === 'Hold') {
// if the only event is a hold event, the call was ended while on hold
// hold time = room.closedAt - event.ts
return closedAt.getTime() - events[0].ts.getTime();
}

let currentOnHoldTime = 0;

for (let i = 0; i < events.length; i += 2) {
const onHold = events[i].ts;
const unHold = events[i + 1]?.ts || closedAt;

currentOnHoldTime += unHold.getTime() - onHold.getTime();
}

return currentOnHoldTime;
}

// Comment can be used to store wrapup call data
async closeRoom(
closerParam: ILivechatVisitor | ILivechatAgent,
room: IVoipRoom,
user: IUser,
comment?: string,
tags?: string[],
sysMessageId: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly' = 'voip-call-wrapup',
options?: { comment?: string; tags?: string[] },
): Promise<boolean> {
this.logger.debug(`Attempting to close room ${room._id}`);
if (!room || room.t !== 'v' || !room.open) {
return false;
}

let { closeInfo, closeSystemMsgData } = await this.getBaseRoomClosingData(closerParam, room, sysMessageId, options);
const finalClosingData = this.getRoomClosingData(closeInfo, closeSystemMsgData, room, sysMessageId, options);
closeInfo = finalClosingData.closeInfo;
closeSystemMsgData = finalClosingData.closeSystemMsgData;

await sendMessage(user, closeSystemMsgData, room);

// There's a race condition between receiving the call and receiving the event
// Sometimes it happens before the connection on client, sometimes it happens after
// For now, this data will be appended as a metric on room closing
await this.setCallWaitingQueueTimers(room);

this.logger.debug(`Room ${room._id} closed and timers set`);
this.logger.debug(`Room ${room._id} was closed at ${closeInfo.closedAt} (duration ${closeInfo.callDuration})`);
this.voipRoom.closeByRoomId(room._id, closeInfo);

return true;
}

getRoomClosingData(
closeInfo: IRoomClosingInfo,
closeSystemMsgData: IOmniRoomClosingMessage,
_room: IVoipRoom,
_sysMessageId: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly',
_options?: { comment?: string; tags?: string[] },
): { closeInfo: IRoomClosingInfo; closeSystemMsgData: IOmniRoomClosingMessage } {
return { closeInfo, closeSystemMsgData };
}

async getBaseRoomClosingData(
closerParam: ILivechatVisitor | ILivechatAgent,
room: IVoipRoom,
sysMessageId: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly',
_options?: { comment?: string; tags?: string[] },
): Promise<{ closeInfo: IRoomClosingInfo; closeSystemMsgData: IOmniRoomClosingMessage }> {
const now = new Date();
const { _id: rid } = room;
const closer = isILivechatVisitor(closerParam) ? 'visitor' : 'user';
const callTotalHoldTime = await this.calculateOnHoldTimeForRoom(room, now);

const closeData: IRoomClosingInfo = {
closedAt: now,
callDuration: now.getTime() - room.ts.getTime(),
closer,
callTotalHoldTime,
tags,
};
this.logger.debug(`Closing room ${room._id} by ${closer} ${closerParam._id}`);
closeData.closedBy = {
_id: closerParam._id,
username: closerParam.username,
closedBy: {
_id: closerParam._id,
username: closerParam.username,
},
};

const message = {
const message: IOmniRoomClosingMessage = {
t: sysMessageId,
msg: comment,
groupable: false,
};

await sendMessage(user, message, room);
// There's a race condition between receiving the call and receiving the event
// Sometimes it happens before the connection on client, sometimes it happens after
// For now, this data will be appended as a metric on room closing
await this.setCallWaitingQueueTimers(room);

this.logger.debug(`Room ${room._id} closed and timers set`);
this.logger.debug(`Room ${room._id} was closed at ${closeData.closedAt} (duration ${closeData.callDuration})`);
this.voipRoom.closeByRoomId(rid, closeData);
return true;
return {
closeInfo: closeData,
closeSystemMsgData: message,
};
}

private getQueuesForExt(
Expand Down
26 changes: 16 additions & 10 deletions packages/rest-typings/src/v1/voip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ const VoipRoomsSchema: JSONSchemaType<VoipRooms> = {

export const isVoipRoomsProps = ajv.compile<VoipRooms>(VoipRoomsSchema);

type VoipRoomClose = { rid: string; token: string; comment: string; tags?: string[] };
type VoipRoomClose = { rid: string; token: string; options: { comment?: string; tags?: string[] } };

const VoipRoomCloseSchema: JSONSchemaType<VoipRoomClose> = {
type: 'object',
Expand All @@ -471,18 +471,24 @@ const VoipRoomCloseSchema: JSONSchemaType<VoipRoomClose> = {
token: {
type: 'string',
},
comment: {
type: 'string',
},
tags: {
type: 'array',
items: {
type: 'string',
options: {
type: 'object',
properties: {
comment: {
type: 'string',
nullable: true,
},
tags: {
type: 'array',
items: {
type: 'string',
},
nullable: true,
},
},
nullable: true,
},
},
required: ['rid', 'token', 'comment'],
required: ['rid', 'token'],
additionalProperties: false,
};

Expand Down

0 comments on commit a741aec

Please sign in to comment.