Skip to content

Commit

Permalink
Merge branch 'improve/voip-admin-cleanup' of ssh://github.com/RocketC…
Browse files Browse the repository at this point in the history
…hat/Rocket.Chat into improve/voip-admin-cleanup
  • Loading branch information
cauefcr committed Jun 28, 2022
2 parents f7684d0 + bf596cf commit 95e66d2
Show file tree
Hide file tree
Showing 26 changed files with 238 additions and 81 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/app/emoji/client/emojiPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ Template.emojiPicker.events({
'click .add-custom'(event) {
event.stopPropagation();
event.preventDefault();
FlowRouter.go('/admin/emoji-custom');
FlowRouter.go('/admin/emoji-custom/new');
EmojiPicker.close();
},
'click .category-link'(event) {
Expand Down
23 changes: 18 additions & 5 deletions apps/meteor/client/providers/CallProvider/CallProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { OutgoingByeRequest } from 'sip.js/lib/core';

import { CustomSounds } from '../../../app/custom-sounds/client';
import { getUserPreference } from '../../../app/utils/client';
import { WrapUpCallModal } from '../../components/voip/modal/WrapUpCallModal';
import { useHasLicense } from '../../../ee/client/hooks/useHasLicense';
import { WrapUpCallModal } from '../../../ee/client/voip/components/modals/WrapUpCallModal';
import { CallContext, CallContextValue } from '../../contexts/CallContext';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { QueueAggregator } from '../../lib/voip/QueueAggregator';
Expand Down Expand Up @@ -53,6 +54,7 @@ export const CallProvider: FC = ({ children }) => {
const result = useVoipClient();
const user = useUser();
const homeRoute = useRoute('home');
const isEnterprise = useHasLicense('voip-enterprise');

const remoteAudioMediaRef = useRef<HTMLAudioElement>(null); // TODO: Create a dedicated file for the AUDIO and make the controls accessible

Expand All @@ -61,7 +63,7 @@ export const CallProvider: FC = ({ children }) => {
const [roomInfo, setRoomInfo] = useState<{ v: { token?: string }; rid: string }>();

const closeRoom = useCallback(
async (data): Promise<void> => {
async (data = {}): Promise<void> => {
roomInfo &&
(await voipCloseRoomEndpoint({
rid: roomInfo.rid,
Expand All @@ -86,6 +88,15 @@ export const CallProvider: FC = ({ children }) => {

const [networkStatus, setNetworkStatus] = useState<NetworkState>('online');

const handleWrapUp = useCallback(() => {
if (isEnterprise) {
openWrapUpModal();
return;
}

closeRoom();
}, [isEnterprise, openWrapUpModal, closeRoom]);

useEffect(() => {
if (!result?.voipClient) {
return;
Expand Down Expand Up @@ -154,12 +165,14 @@ export const CallProvider: FC = ({ children }) => {

const handleCallHangup = (_event: { roomId: string }): void => {
setQueueName(queueAggregator.getCurrentQueueName());
openWrapUpModal();

handleWrapUp();

dispatchEvent({ event: VoipClientEvents['VOIP-CALL-ENDED'], rid: _event.roomId });
};

return subscribeToNotifyUser(`${user._id}/call.hangup`, handleCallHangup);
}, [openWrapUpModal, queueAggregator, subscribeToNotifyUser, user, voipEnabled, dispatchEvent]);
}, [openWrapUpModal, queueAggregator, subscribeToNotifyUser, user, voipEnabled, dispatchEvent, handleWrapUp]);

useEffect(() => {
if (!result.voipClient) {
Expand Down Expand Up @@ -325,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
2 changes: 2 additions & 0 deletions apps/meteor/ee/app/license/server/bundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type BundleFeature =
| 'canned-responses'
| 'ldap-enterprise'
| 'livechat-enterprise'
| 'voip-enterprise'
| 'omnichannel-mobile-enterprise'
| 'engagement-dashboard'
| 'push-privacy'
Expand All @@ -21,6 +22,7 @@ const bundles: IBundle = {
'canned-responses',
'ldap-enterprise',
'livechat-enterprise',
'voip-enterprise',
'omnichannel-mobile-enterprise',
'engagement-dashboard',
'push-privacy',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
import { escapeRegExp } from '@rocket.chat/string-helpers';
import LivechatTag from '@rocket.chat/models';
import { LivechatTag } from '@rocket.chat/models';
import { ILivechatTag } from '@rocket.chat/core-typings';

import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission';

export async function findTags({ userId, text, pagination: { offset, count, sort } }) {
type FindTagsParams = {
userId: string;
text: string;
pagination: {
offset: number;
count: number;
sort: object;
};
};

type FindTagsResult = {
tags: ILivechatTag[];
count: number;
offset: number;
total: number;
};

type FindTagsByIdParams = {
userId: string;
tagId: string;
};

type FindTagsByIdResult = ILivechatTag | null;

export async function findTags({ userId, text, pagination: { offset, count, sort } }: FindTagsParams): Promise<FindTagsResult> {
if (!(await hasPermissionAsync(userId, 'manage-livechat-tags')) && !(await hasPermissionAsync(userId, 'view-l-room'))) {
throw new Error('error-not-authorized');
}
Expand All @@ -28,7 +53,7 @@ export async function findTags({ userId, text, pagination: { offset, count, sort
};
}

export async function findTagById({ userId, tagId }) {
export async function findTagById({ userId, tagId }: FindTagsByIdParams): Promise<FindTagsByIdResult> {
if (!(await hasPermissionAsync(userId, 'manage-livechat-tags')) && !(await hasPermissionAsync(userId, 'view-l-room'))) {
throw new Error('error-not-authorized');
}
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/ee/app/livechat-enterprise/server/api/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ API.v1.addRoute(
pagination: {
offset,
count,
sort,
sort: JSON.parse(sort || '{}'),
},
}),
),
Expand Down
18 changes: 0 additions & 18 deletions apps/meteor/ee/client/hooks/useHasLicense.js

This file was deleted.

14 changes: 14 additions & 0 deletions apps/meteor/ee/client/hooks/useHasLicense.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useState, useEffect } from 'react';

import { hasLicense } from '../../app/license/client';
import { BundleFeature } from '../../app/license/server/bundles';

export const useHasLicense = (licenseName: BundleFeature): 'loading' | boolean => {
const [license, setLicense] = useState<'loading' | boolean>('loading');

useEffect(() => {
hasLicense(licenseName).then(setLicense);
}, [licenseName]);

return license;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement, useEffect } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';

import Tags from '../../Omnichannel/Tags';
import Tags from '../../../../../client/components/Omnichannel/Tags';

type WrapUpCallPayload = {
comment: string;
Expand Down Expand Up @@ -42,8 +42,6 @@ export const WrapUpCallModal = ({ closeRoom }: WrapUpCallModalProps): ReactEleme
closeModal();
};

useEffect(() => closeRoom, [closeRoom]);

return (
<Modal is='form' onSubmit={handleSubmit(onSubmit)}>
<Modal.Header>
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/ee/server/models/raw/LivechatTag.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { IRocketChatRecord } from '@rocket.chat/core-typings';
import { ILivechatTag } from '@rocket.chat/core-typings';
import type { ILivechatTagModel } from '@rocket.chat/model-typings';
import { getCollectionName } from '@rocket.chat/models';
import { Db } from 'mongodb';

import { BaseRaw } from '../../../../server/models/raw/BaseRaw';

export class LivechatTagRaw extends BaseRaw<IRocketChatRecord> implements ILivechatTagModel {
export class LivechatTagRaw extends BaseRaw<ILivechatTag> implements ILivechatTagModel {
constructor(db: Db) {
super(db, getCollectionName('livechat_tag'));
}
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@
"@nivo/line": "0.62.0",
"@nivo/pie": "0.73.0",
"@rocket.chat/api-client": "workspace:^",
"@rocket.chat/apps-engine": "1.32.0",
"@rocket.chat/apps-engine": "1.33.0-alpha.6451",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/css-in-js": "~0.31.12",
"@rocket.chat/emitter": "~0.31.12",
Expand Down
Loading

0 comments on commit 95e66d2

Please sign in to comment.