[IMPROVE] Differ Voip calls from Incoming and Outgoing (#25643)

<!-- 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>
pull/26007/head^2
Murtaza Patrawala 4 years ago committed by GitHub
parent 20f4382798
commit acdb7f93d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      apps/meteor/app/api/server/v1/voip/rooms.ts
  2. 2
      apps/meteor/client/providers/CallProvider/CallProvider.tsx
  3. 26
      apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx
  4. 1
      apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
  5. 11
      apps/meteor/server/models/raw/VoipRoom.ts
  6. 1
      apps/meteor/server/sdk/types/IOmnichannelVoipService.ts
  7. 4
      apps/meteor/server/services/omnichannel-voip/internalTypes.ts
  8. 9
      apps/meteor/server/services/omnichannel-voip/service.ts
  9. 1
      apps/meteor/server/startup/migrations/index.ts
  10. 21
      apps/meteor/server/startup/migrations/v270.ts
  11. 4
      apps/meteor/tests/data/rooms.helper.js
  12. 2
      packages/core-typings/src/IRoom.ts
  13. 4
      packages/model-typings/src/models/IVoipRoomModel.ts
  14. 37
      packages/rest-typings/src/v1/voip.ts

@ -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';
@ -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
@ -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');
@ -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 });
@ -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;
@ -170,6 +184,8 @@ API.v1.addRoute(
visitorId,
createdAt,
closedAt,
direction,
roomName,
options: { sort, offset, count, fields },
}),
);

@ -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();

@ -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';
@ -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;
@ -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}>
@ -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 (

@ -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",

@ -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';
@ -114,6 +115,8 @@ export class VoipRoomRaw extends BaseRaw<IVoipRoom> implements IVoipRoomModel {
tags,
queue,
visitorId,
direction,
roomName,
options = {},
}: {
agents?: string[];
@ -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;
@ -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 },

@ -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>;

@ -1,4 +1,4 @@
import { IMessage } from '@rocket.chat/core-typings';
import { IVoipRoom, IMessage } from '@rocket.chat/core-typings';
export type FindVoipRoomsParams = {
agents?: string[];
@ -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'>>;

@ -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;
@ -161,6 +162,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
_id: agent.agentId,
username: agent.username,
},
direction,
_updatedAt: newRoomAt,
};
@ -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}`);
@ -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}`);
@ -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({
@ -381,6 +386,8 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
tags,
queue,
visitorId,
direction,
roomName,
options: {
sort: sort || { ts: -1 },
offset,

@ -93,5 +93,6 @@ import './v266';
import './v267';
import './v268';
import './v269';
import './v270';
import './v271';
import './xrun';

@ -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',
},
},
);
},
});

@ -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');
}
@ -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();
}

@ -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 {

@ -23,6 +23,8 @@ export interface IVoipRoomModel extends IBaseModel<IVoipRoom> {
tags,
queue,
visitorId,
direction,
roomName,
options,
}: {
agents?: string[];
@ -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;

@ -320,7 +320,7 @@ const VoipEventsSchema: JSONSchemaType<VoipEvents> = {
export const isVoipEventsProps = ajv.compile<VoipEvents>(VoipEventsSchema);
type VoipRoom = { token: string; agentId: ILivechatAgent['_id'] } | { rid: string; token: string };
type VoipRoom = { token: string; agentId: ILivechatAgent['_id']; direction: IVoipRoom['direction'] } | { rid: string; token: string };
const VoipRoomSchema: JSONSchemaType<VoipRoom> = {
oneOf: [
@ -333,6 +333,10 @@ const VoipRoomSchema: JSONSchemaType<VoipRoom> = {
agentId: {
type: 'string',
},
direction: {
type: 'string',
enum: ['inbound', 'outbound'],
},
},
required: ['token', 'agentId'],
additionalProperties: false,
@ -405,7 +409,7 @@ const VoipCallServerCheckConnectionSchema: JSONSchemaType<VoipCallServerCheckCon
export const isVoipCallServerCheckConnectionProps = ajv.compile<VoipCallServerCheckConnection>(VoipCallServerCheckConnectionSchema);
type VoipRooms = {
type VoipRooms = PaginatedRequest<{
agents?: string[];
open?: 'true' | 'false';
createdAt?: string;
@ -413,7 +417,9 @@ type VoipRooms = {
tags?: string[];
queue?: string;
visitorId?: string;
};
roomName?: string;
direction?: IVoipRoom['direction'];
}>;
const VoipRoomsSchema: JSONSchemaType<VoipRooms> = {
type: 'object',
@ -453,6 +459,31 @@ const VoipRoomsSchema: JSONSchemaType<VoipRooms> = {
type: 'string',
nullable: true,
},
direction: {
type: 'string',
enum: ['inbound', 'outbound'],
nullable: true,
},
roomName: {
type: 'string',
nullable: true,
},
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
query: {
type: 'string',
nullable: true,
},
},
required: [],
additionalProperties: false,

Loading…
Cancel
Save