Skip to content

Commit

Permalink
[IMPROVE] Differ Voip calls from Incoming and Outgoing (#25643)
Browse files Browse the repository at this point in the history
<!-- 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.
-->
Updated this column and its respective endpoints to support inbound/outfound call definitions
![image](https://user-images.githubusercontent.com/34130764/170512008-34202ed8-3ed4-4c28-baa5-25efc17543d5.png)


<!-- 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... -->
Clickup: https://app.clickup.com/t/22bmc0f

Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com>
  • Loading branch information
murtaza98 and KevLehman committed Jun 27, 2022
1 parent 20f4382 commit acdb7f9
Show file tree
Hide file tree
Showing 14 changed files with 144 additions and 35 deletions.
56 changes: 36 additions & 20 deletions apps/meteor/app/api/server/v1/voip/rooms.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Match, check } from 'meteor/check';
import { Random } from 'meteor/random';
import type { ILivechatAgent } from '@rocket.chat/core-typings';
import type { ILivechatAgent, IVoipRoom } from '@rocket.chat/core-typings';
import { isVoipRoomProps, isVoipRoomsProps, isVoipRoomCloseProps } from '@rocket.chat/rest-typings/dist/v1/voip';
import { VoipRoom, LivechatVisitors, Users } from '@rocket.chat/models';
import { isVoipRoomCloseProps } from '@rocket.chat/rest-typings/dist/v1/voip';

import { API } from '../../api';
import { LivechatVoip } from '../../../../../server/sdk';
Expand All @@ -25,6 +24,7 @@ const validateDateParams = (property: string, date: DateParam = {}): DateParam =
const parseAndValidate = (property: string, date?: string): DateParam => {
return validateDateParams(property, parseDateParams(date));
};

/**
* @openapi
* /voip/server/api/v1/voip/room
Expand Down Expand Up @@ -81,23 +81,38 @@ const parseAndValidate = (property: string, date?: string): DateParam => {
* $ref: '#/components/schemas/ApiFailureV1'
*/

const isRoomSearchProps = (props: any): props is { rid: string; token: string } => {
return 'rid' in props && 'token' in props;
};

const isRoomCreationProps = (props: any): props is { agentId: string; direction: IVoipRoom['direction'] } => {
return 'agentId' in props && 'direction' in props;
};

API.v1.addRoute(
'voip/room',
{
authRequired: true,
rateLimiterOptions: { numRequestsAllowed: 5, intervalTimeInMS: 60000 },
permissionsRequired: ['inbound-voip-calls'],
validateParams: isVoipRoomProps,
},
{
async get() {
const defaultCheckParams = {
token: String,
agentId: Match.Maybe(String),
rid: Match.Maybe(String),
};
check(this.queryParams, defaultCheckParams);

const { token, rid, agentId } = this.queryParams;
const { token } = this.queryParams;
let agentId: string | undefined = undefined;
let direction: IVoipRoom['direction'] = 'inbound';
let rid: string | undefined = undefined;

if (isRoomCreationProps(this.queryParams)) {
agentId = this.queryParams.agentId;
direction = this.queryParams.direction;
}

if (isRoomSearchProps(this.queryParams)) {
rid = this.queryParams.rid;
}

const guest = await LivechatVisitors.getVisitorByToken(token, {});
if (!guest) {
return API.v1.failure('invalid-token');
Expand All @@ -123,7 +138,11 @@ API.v1.addRoute(
const agent = { agentId: _id, username };
const rid = Random.id();

return API.v1.success(await LivechatVoip.getNewRoom(guest, agent, rid, { projection: API.v1.defaultFieldsToExclude }));
return API.v1.success(
await LivechatVoip.getNewRoom(guest, agent, rid, direction, {
projection: API.v1.defaultFieldsToExclude,
}),
);
}

const room = await VoipRoom.findOneByIdAndVisitorToken(rid, token, { projection: API.v1.defaultFieldsToExclude });
Expand All @@ -137,20 +156,15 @@ API.v1.addRoute(

API.v1.addRoute(
'voip/rooms',
{ authRequired: true },
{ authRequired: true, validateParams: isVoipRoomsProps },
{
async get() {
const { offset, count } = this.getPaginationItems();

const { sort, fields } = this.parseJsonQuery();
const { agents, open, tags, queue, visitorId } = this.requestParams();
const { agents, open, tags, queue, visitorId, direction, roomName } = this.requestParams();
const { createdAt: createdAtParam, closedAt: closedAtParam } = this.requestParams();

check(agents, Match.Maybe([String]));
check(open, Match.Maybe(String));
check(tags, Match.Maybe([String]));
check(queue, Match.Maybe(String));
check(visitorId, Match.Maybe(String));

// Reusing same L room permissions for simplicity
const hasAdminAccess = hasPermission(this.userId, 'view-livechat-rooms');
const hasAgentAccess = hasPermission(this.userId, 'view-l-room') && agents?.includes(this.userId) && agents?.length === 1;
Expand All @@ -170,6 +184,8 @@ API.v1.addRoute(
visitorId,
createdAt,
closedAt,
direction,
roomName,
options: { sort, offset, count, fields },
}),
);
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/providers/CallProvider/CallProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ export const CallProvider: FC = ({ children }) => {
name: caller.callerName || caller.callerId,
},
});
const voipRoom = visitor && (await voipEndpoint({ token: visitor.token, agentId: user._id }));
const voipRoom = visitor && (await voipEndpoint({ token: visitor.token, agentId: user._id, direction: 'inbound' }));
openRoom(voipRoom.room._id);
voipRoom.room && setRoomInfo({ v: { token: voipRoom.room.v.token }, rid: voipRoom.room._id });
const queueAggregator = voipClient.getAggregator();
Expand Down
26 changes: 19 additions & 7 deletions apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IVoipRoom } from '@rocket.chat/core-typings';
import { Table } from '@rocket.chat/fuselage';
import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
Expand Down Expand Up @@ -54,6 +55,17 @@ const CallTable: FC = () => {
const query = useQuery(debouncedParams, debouncedSort, userIdLoggedIn);
const directoryRoute = useRoute('omnichannel-directory');

const resolveDirectionLabel = useCallback(
(direction: IVoipRoom['direction']) => {
const labels = {
inbound: 'Incoming',
outbound: 'Outgoing',
} as const;
return t(labels[direction] || 'Not_Available');
},
[t],
);

const onHeaderClick = useMutableCallback((id) => {
const [sortBy, sortDirection] = sort;

Expand Down Expand Up @@ -117,21 +129,21 @@ const CallTable: FC = () => {
{t('Talk_Time')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell
key={'source'}
key='direction'
direction={sort[1]}
active={sort[0] === 'source'}
active={sort[0] === 'direction'}
onClick={onHeaderClick}
sort='source'
sort='direction'
w='x200'
>
{t('Source')}
{t('Direction')}
</GenericTable.HeaderCell>,
].filter(Boolean),
[sort, onHeaderClick, t],
);

const renderRow = useCallback(
({ _id, fname, callStarted, queue, callDuration, v }) => {
({ _id, fname, callStarted, queue, callDuration, v, direction }) => {
const duration = moment.duration(callDuration / 1000, 'seconds');
return (
<Table.Row key={_id} tabIndex={0} role='link' onClick={(): void => onRowClick(_id, v?.token)} action qa-user-id={_id}>
Expand All @@ -140,11 +152,11 @@ const CallTable: FC = () => {
<Table.Cell withTruncatedText>{queue}</Table.Cell>
<Table.Cell withTruncatedText>{moment(callStarted).format('L LTS')}</Table.Cell>
<Table.Cell withTruncatedText>{duration.isValid() && duration.humanize()}</Table.Cell>
<Table.Cell withTruncatedText>{t('Incoming')}</Table.Cell>
<Table.Cell withTruncatedText>{resolveDirectionLabel(direction)}</Table.Cell>
</Table.Row>
);
},
[onRowClick, t],
[onRowClick, resolveDirectionLabel],
);

return (
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1459,6 +1459,7 @@
"Details": "Details",
"Different_Style_For_User_Mentions": "Different style for user mentions",
"Direct": "Direct",
"Direction": "Direction",
"Direct_Message": "Direct Message",
"Direct_message_creation_description": "You are about to create a chat with multiple users. Add the ones you would like to talk, everyone in the same place, using direct messages.",
"Direct_message_someone": "Direct message someone",
Expand Down
11 changes: 11 additions & 0 deletions apps/meteor/server/models/raw/VoipRoom.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { IRoomClosingInfo, IVoipRoom, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
import type { IVoipRoomModel } from '@rocket.chat/model-typings';
import type { Collection, Cursor, Db, FilterQuery, FindOneOptions, WithoutProjection, WriteOpResult } from 'mongodb';
Expand Down Expand Up @@ -114,6 +115,8 @@ export class VoipRoomRaw extends BaseRaw<IVoipRoom> implements IVoipRoomModel {
tags,
queue,
visitorId,
direction,
roomName,
options = {},
}: {
agents?: string[];
Expand All @@ -123,6 +126,8 @@ export class VoipRoomRaw extends BaseRaw<IVoipRoom> implements IVoipRoomModel {
tags?: string[];
queue?: string;
visitorId?: string;
direction?: IVoipRoom['direction'];
roomName?: string;
options?: {
sort?: Record<string, unknown>;
count?: number;
Expand Down Expand Up @@ -167,6 +172,12 @@ export class VoipRoomRaw extends BaseRaw<IVoipRoom> implements IVoipRoomModel {
if (queue) {
query.queue = queue;
}
if (direction) {
query.direction = direction;
}
if (roomName) {
query.name = new RegExp(escapeRegExp(roomName), 'i');
}

return this.find(query, {
sort: options.sort || { name: 1 },
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/server/sdk/types/IOmnichannelVoipService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface IOmnichannelVoipService {
guest: ILivechatVisitor,
agent: { agentId: string; username: string },
rid: string,
direction: IVoipRoom['direction'],
options: FindOneOptions<IVoipRoom>,
): Promise<IRoomCreationResponse>;
findRoom(token: string, rid: string): Promise<IVoipRoom | null>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IMessage } from '@rocket.chat/core-typings';
import { IVoipRoom, IMessage } from '@rocket.chat/core-typings';

export type FindVoipRoomsParams = {
agents?: string[];
Expand All @@ -14,6 +14,8 @@ export type FindVoipRoomsParams = {
fields?: Record<string, unknown>;
offset?: number;
};
direction?: IVoipRoom['direction'];
roomName?: string;
};

export type IOmniRoomClosingMessage = Pick<IMessage, 't' | 'groupable'> & Partial<Pick<IMessage, 'msg'>>;
9 changes: 8 additions & 1 deletion apps/meteor/server/services/omnichannel-voip/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
name: string,
agent: { agentId: string; username: string },
guest: ILivechatVisitor,
direction: IVoipRoom['direction'],
): Promise<string> {
const status = 'online';
const { _id, department: departmentId } = guest;
Expand Down Expand Up @@ -161,6 +162,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
_id: agent.agentId,
username: agent.username,
},
direction,
_updatedAt: newRoomAt,
};

Expand Down Expand Up @@ -213,6 +215,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
guest: ILivechatVisitor,
agent: { agentId: string; username: string },
rid: string,
direction: IVoipRoom['direction'],
options: FindOneOptions<IVoipRoom> = {},
): Promise<IRoomCreationResponse> {
this.logger.debug(`Attempting to find or create a room for visitor ${guest._id}`);
Expand All @@ -224,7 +227,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
}
if (room == null) {
const name = guest.name || guest.username;
const roomId = await this.createVoipRoom(rid, name, agent, guest);
const roomId = await this.createVoipRoom(rid, name, agent, guest, direction);
room = await VoipRoom.findOneVoipRoomById(roomId);
newRoom = true;
this.logger.debug(`Room obtained for visitor ${guest._id} -> ${room?._id}`);
Expand Down Expand Up @@ -371,6 +374,8 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
visitorId,
tags,
queue,
direction,
roomName,
options: { offset = 0, count, fields, sort } = {},
}: FindVoipRoomsParams): Promise<PaginatedResult<{ rooms: IVoipRoom[] }>> {
const cursor = VoipRoom.findRoomsWithCriteria({
Expand All @@ -381,6 +386,8 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
tags,
queue,
visitorId,
direction,
roomName,
options: {
sort: sort || { ts: -1 },
offset,
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/server/startup/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,6 @@ import './v266';
import './v267';
import './v268';
import './v269';
import './v270';
import './v271';
import './xrun';
21 changes: 21 additions & 0 deletions apps/meteor/server/startup/migrations/v270.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { VoipRoom } from '@rocket.chat/models';

import { addMigration } from '../../lib/migrations';

addMigration({
version: 270,
async up() {
// mark all voip rooms as inbound which doesn't have any direction property set or has an invalid value
await VoipRoom.updateMany(
{
t: 'v',
$or: [{ direction: { $exists: false } }, { direction: { $nin: ['inbound', 'outbound'] } }],
},
{
$set: {
direction: 'inbound',
},
},
);
},
});
4 changes: 2 additions & 2 deletions apps/meteor/tests/data/rooms.helper.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { api, credentials, request } from './api-data';

export const createRoom = ({ name, type, username, token, agentId, members, credentials: customCredentials }) => {
export const createRoom = ({ name, type, username, token, agentId, members, credentials: customCredentials, voipCallDirection = 'inbound' }) => {
if (!type) {
throw new Error('"type" is required in "createRoom.ts" test helper');
}
Expand All @@ -11,7 +11,7 @@ export const createRoom = ({ name, type, username, token, agentId, members, cred
* is handled separately here.
*/
return request
.get(api(`voip/room?token=${token}&agentId=${agentId}`))
.get(api(`voip/room?token=${token}&agentId=${agentId}&direction=${voipCallDirection}`))
.set(customCredentials || credentials)
.send();
}
Expand Down
2 changes: 2 additions & 0 deletions packages/core-typings/src/IRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ export interface IVoipRoom extends IOmnichannelGenericRoom {
status: 'online' | 'busy' | 'away' | 'offline';
phone?: string | null;
};
// Outbound means the call was initiated from Rocket.Chat and vise versa
direction: 'inbound' | 'outbound';
}

export interface IOmnichannelRoomFromAppSource extends IOmnichannelRoom {
Expand Down
4 changes: 4 additions & 0 deletions packages/model-typings/src/models/IVoipRoomModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface IVoipRoomModel extends IBaseModel<IVoipRoom> {
tags,
queue,
visitorId,
direction,
roomName,
options,
}: {
agents?: string[];
Expand All @@ -32,6 +34,8 @@ export interface IVoipRoomModel extends IBaseModel<IVoipRoom> {
tags?: string[];
queue?: string;
visitorId?: string;
direction?: IVoipRoom['direction'];
roomName?: string;
options?: {
sort?: Record<string, unknown>;
count?: number;
Expand Down
Loading

0 comments on commit acdb7f9

Please sign in to comment.