Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
Browse files Browse the repository at this point in the history
…nto custom-sound
  • Loading branch information
himanshu-malviya15 committed Feb 26, 2022
2 parents 62ea1b5 + 4053ae1 commit 44819b0
Show file tree
Hide file tree
Showing 20 changed files with 295 additions and 97 deletions.
39 changes: 29 additions & 10 deletions app/api/server/v1/voip/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Match, check } from 'meteor/check';

import { API } from '../../api';
import { hasPermission } from '../../../../authorization/server/index';
import { Users } from '../../../../models/server/raw/index';
import { Voip } from '../../../../../server/sdk';
import { IVoipExtensionBase } from '../../../../../definition/IVoipExtension';
import { generateJWT } from '../../../../utils/server/lib/JWTHelper';
import { settings } from '../../../../settings/server';
import { logger } from './logger';

// Get the connector version and type
API.v1.addRoute(
'connector.getVersion',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['manage-voip-call-settings'] },
{
async get() {
const version = await Voip.getConnectorVersion();
Expand All @@ -21,7 +23,7 @@ API.v1.addRoute(
// Get the extensions available on the call server
API.v1.addRoute(
'connector.extension.list',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['manage-voip-call-settings'] },
{
async get() {
const list = await Voip.getExtensionList();
Expand All @@ -37,7 +39,7 @@ API.v1.addRoute(
*/
API.v1.addRoute(
'connector.extension.getDetails',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['manage-voip-call-settings'] },
{
async get() {
check(
Expand All @@ -57,7 +59,7 @@ API.v1.addRoute(

API.v1.addRoute(
'connector.extension.getRegistrationInfoByExtension',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['manage-voip-call-settings'] },
{
async get() {
check(
Expand All @@ -67,14 +69,21 @@ API.v1.addRoute(
}),
);
const endpointDetails = await Voip.getRegistrationInfo(this.requestParams());
return API.v1.success({ ...endpointDetails.result });
const encKey = settings.get('VoIP_JWT_Secret');
if (!encKey) {
logger.warn('No JWT keys set. Sending registration info as plain text');
return API.v1.success({ ...endpointDetails.result });
}

const result = generateJWT(endpointDetails.result, encKey);
return API.v1.success({ result });
},
},
);

API.v1.addRoute(
'connector.extension.getRegistrationInfoByUserId',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['view-agent-extension-association'] },
{
async get() {
check(
Expand All @@ -83,10 +92,12 @@ API.v1.addRoute(
id: String,
}),
);
if (!hasPermission(this.userId, 'view-agent-extension-association')) {
const { id } = this.requestParams();

if (id !== this.userId) {
return API.v1.unauthorized();
}
const { id } = this.requestParams();

const { extension } =
(await Users.getVoipExtensionByUserId(id, {
projection: {
Expand All @@ -99,8 +110,16 @@ API.v1.addRoute(
if (!extension) {
return API.v1.notFound('Extension not found');
}

const endpointDetails = await Voip.getRegistrationInfo({ extension });
return API.v1.success({ ...endpointDetails.result });
const encKey = settings.get('VoIP_JWT_Secret');
if (!encKey) {
logger.warn('No JWT keys set. Sending registration info as plain text');
return API.v1.success({ ...endpointDetails.result });
}

const result = generateJWT(endpointDetails.result, encKey);
return API.v1.success({ result });
},
},
);
11 changes: 9 additions & 2 deletions app/lib/server/startup/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3150,10 +3150,17 @@ settingsRegistry.addGroup('Call_Center', function () {
value: true,
},
});

this.add('VoIP_JWT_Secret', '', {
type: 'password',
i18nDescription: 'VoIP_JWT_Secret_description',
enableQuery: {
_id: 'VoIP_Enabled',
value: true,
},
});
this.section('Server_Configuration', function () {
this.add('VoIP_Server_Host', '', {
type: 'string',
type: 'password',
public: true,
enableQuery: {
_id: 'VoIP_Enabled',
Expand Down
12 changes: 6 additions & 6 deletions app/livechat/client/views/app/livechatReadOnly.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template name="livechatReadOnly">
{{#if roomOpen}}
{{#if isPreparing}}
{{> loading}}
{{else}}
{{#if isPreparing}}
{{> loading}}
{{else}}
{{#if roomOpen}}
{{#if isOnHold}}
<div class="rc-message-box__join">
{{{_ "chat_on_hold_due_to_inactivity"}}}
Expand All @@ -21,8 +21,8 @@
</div>
{{/if}}
{{/if}}
{{else}}
<p>{{_ "This_conversation_is_already_closed"}}</p>
{{/if}}
{{else}}
<p>{{_ "This_conversation_is_already_closed"}}</p>
{{/if}}
</template>
6 changes: 6 additions & 0 deletions app/voip/server/startup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { settings } from '../../settings/server';
import { Voip } from '../../../server/sdk';

settings.watch('VoIP_Enabled', (value: boolean) => {
return value ? Voip.init() : Voip.stop();
});
13 changes: 10 additions & 3 deletions client/components/voip/modal/WrapUpCallModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export const WrapUpCallModal = (): ReactElement => {
const closeModal = (): void => setModal(null);
const t = useTranslation();

const { register, handleSubmit, setValue } = useForm<WrapUpCallPayload>();
const { register, handleSubmit, setValue, watch } = useForm<WrapUpCallPayload>();

const tags = watch('tags');

useEffect(() => {
register('tags');
Expand All @@ -34,6 +36,11 @@ export const WrapUpCallModal = (): ReactElement => {
closeModal();
};

const onCancel = (): void => {
closeRoom({});
closeModal();
};

return (
<Modal is='form' onSubmit={handleSubmit(onSubmit)}>
<Modal.Header>
Expand All @@ -48,11 +55,11 @@ export const WrapUpCallModal = (): ReactElement => {
</Field.Row>
<Field.Hint>{t('These_notes_will_be_available_in_the_call_summary')}</Field.Hint>
</Field>
<Tags handler={handleTags as () => void} />
<Tags tags={tags} handler={handleTags as () => void} />
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={closeModal}>
<Button ghost onClick={onCancel}>
{t('Cancel')}
</Button>
<Button type='submit' primary>
Expand Down
2 changes: 1 addition & 1 deletion client/contexts/CallContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type CallContextReady = {
openedRoomInfo: { v: { token?: string }; rid: string };
openWrapUpModal: () => void;
openRoom: (caller: ICallerInfo) => IVoipRoom['_id'];
closeRoom: (data: { comment: string; tags?: string[] }) => void;
closeRoom: (data: { comment?: string; tags?: string[] }) => void;
};
type CallContextError = {
enabled: true;
Expand Down
2 changes: 1 addition & 1 deletion client/providers/CallProvider/CallProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export const CallProvider: FC = ({ children }) => {
return '';
},
closeRoom: async ({ comment, tags }): Promise<void> => {
roomInfo && (await voipCloseRoomEndpoint({ rid: roomInfo.rid, token: roomInfo.v.token || '', comment, tags }));
roomInfo && (await voipCloseRoomEndpoint({ rid: roomInfo.rid, token: roomInfo.v.token || '', comment: comment || '', tags }));
homeRoute.push({});
},
openWrapUpModal,
Expand Down
16 changes: 14 additions & 2 deletions client/providers/CallProvider/hooks/useVoipClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useSafely } from '@rocket.chat/fuselage-hooks';
import { KJUR } from 'jsrsasign';
import { useEffect, useState } from 'react';

import { IRegistrationInfo } from '../../../../definition/voip/IRegistrationInfo';
Expand All @@ -23,6 +24,8 @@ export const isUseVoipClientResultError = (result: UseVoipClientResult): result
export const isUseVoipClientResultLoading = (result: UseVoipClientResult): result is UseVoipClientResultLoading =>
!result || !Object.keys(result).length;

const isSignedResponse = (data: any): data is { result: string } => typeof data?.result === 'string';

export const useVoipClient = (): UseVoipClientResult => {
const registrationInfo = useEndpoint('GET', 'connector.extension.getRegistrationInfoByUserId');
const user = useUser();
Expand All @@ -37,16 +40,25 @@ export const useVoipClient = (): UseVoipClientResult => {
}
registrationInfo({ id: user._id }).then(
(data) => {
let parsedData: IRegistrationInfo;
if (isSignedResponse(data)) {
const result = KJUR.jws.JWS.parse(data.result);
parsedData = (result.payloadObj as any)?.context as IRegistrationInfo;
} else {
parsedData = data;
}

const {
extensionDetails: { extension, password },
host,
callServerConfig: { websocketPath },
} = data;
} = parsedData;

let client: VoIPUser;
(async (): Promise<void> => {
try {
client = await SimpleVoipUser.create(extension, password, host, websocketPath, iceServers, 'video');
setResult({ voipClient: client, registrationInfo: data });
setResult({ voipClient: client, registrationInfo: parsedData });
} catch (e) {
setResult({ error: e as Error });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Icon } from '@rocket.chat/fuselage';
import { Box, Icon, Chip } from '@rocket.chat/fuselage';
import moment from 'moment';
import React, { ReactElement } from 'react';
import React, { ReactElement, useMemo } from 'react';

import { IVoipRoom } from '../../../../../../definition/IRoom';
import UserCard from '../../../../../components/UserCard';
Expand All @@ -22,12 +22,14 @@ type VoipInfoPropsType = {
export const VoipInfo = ({ room, onClickClose /* , onClickReport, onClickCall */ }: VoipInfoPropsType): ReactElement => {
const t = useTranslation();

const { servedBy, queue, v, fname, name, callDuration, callTotalHoldTime, callEndedAt, callWaitingTime } = room;
const duration = callDuration && moment.duration(callDuration / 1000, 'seconds').humanize();
const waiting = callWaitingTime && moment.duration(callWaitingTime / 1000, 'seconds').humanize();
const hold = callTotalHoldTime && moment.duration(callTotalHoldTime, 'seconds').humanize();
const { servedBy, queue, v, fname, name, callDuration, callTotalHoldTime, callEndedAt, callWaitingTime, tags, lastMessage } = room;
const duration = callDuration && moment.utc(callDuration).format('HH:mm:ss');
const waiting = callWaitingTime && moment.utc(callWaitingTime).format('HH:mm:ss');
const hold = callTotalHoldTime && moment.utc(callTotalHoldTime).format('HH:mm:ss');
const endedAt = callEndedAt && moment(callEndedAt).format('LLL');
const phoneNumber = Array.isArray(v?.phone) ? v?.phone[0]?.phoneNumber : v?.phone;
const shouldShowWrapup = useMemo(() => lastMessage?.t === 'voip-call-wrapup' && lastMessage?.msg, [lastMessage]);
const shouldShowTags = useMemo(() => tags && tags.length > 0, [tags]);

return (
<>
Expand Down Expand Up @@ -61,9 +63,22 @@ export const VoipInfo = ({ room, onClickClose /* , onClickReport, onClickCall */
<InfoField label={t('Waiting_Time')} info={waiting || t('Not_Available')} />
<InfoField label={t('Talk_Time')} info={duration || t('Not_Available')} />
<InfoField label={t('Hold_Time')} info={hold || t('Not_Available')} />
<InfoPanel.Field>
<InfoPanel.Label>{t('Wrap_Up_Notes')}</InfoPanel.Label>
<InfoPanel.Text>{shouldShowWrapup ? lastMessage?.msg : t('Not_Available')}</InfoPanel.Text>
{shouldShowTags && (
<InfoPanel.Text>
<Box display='flex' flexDirection='row' alignItems='center'>
{tags?.map((tag: string) => (
<Chip mie='x4' key={tag} value={tag}>
{tag}
</Chip>
))}
</Box>
</InfoPanel.Text>
)}
</InfoPanel.Field>
</InfoPanel>

{/* <InfoField label={t('Wrap_Up_Note')} info={guest.holdTime} /> */}
</VerticalBar.ScrollableContent>
<VerticalBar.Footer>
{/* TODO: Introduce this buttons [Not part of MVP] */}
Expand Down
2 changes: 1 addition & 1 deletion definition/rest/v1/voip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PaginatedResult } from '../helpers/PaginatedResult';

export type VoipEndpoints = {
'connector.extension.getRegistrationInfoByUserId': {
GET: (params: { id: string }) => IRegistrationInfo;
GET: (params: { id: string }) => IRegistrationInfo | { result: string };
};
'voip/queues.getSummary': {
GET: () => { summary: IQueueSummary[] };
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@
"@types/dompurify": "^2.2.2",
"@types/ejson": "^2.1.3",
"@types/express": "^4.17.12",
"@types/google-libphonenumber": "^7.4.21",
"@types/fibers": "^3.1.1",
"@types/google-libphonenumber": "^7.4.21",
"@types/imap": "^0.8.35",
"@types/jsdom": "^16.2.12",
"@types/jsdom-global": "^3.0.2",
"@types/jsrsasign": "^9.0.1",
"@types/jsrsasign": "^9.0.3",
"@types/ldapjs": "^2.2.1",
"@types/less": "^3.0.2",
"@types/lodash.get": "^4.4.6",
Expand Down
3 changes: 3 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -4737,6 +4737,8 @@
"Voip_call_ended": "Call ended at",
"Voip_call_ended_unexpectedly": "Call ended unexpectedly: __reason__",
"Voip_call_wrapup": "Call wrapup notes added: __comment__",
"VoIP_JWT_Secret": "VoIP JWT Secret",
"VoIP_JWT_Secret_description": "This allows you to set a secret key for sharing extension details from server to client as JWT instead of plain text. If you don't setup this, extension registration details will be sent as plain text",
"Chat_opened_by_visitor": "Chat opened by the visitor",
"Wait_activation_warning": "Before you can login, your account must be manually activated by an administrator.",
"Waiting_queue": "Waiting queue",
Expand Down Expand Up @@ -4797,6 +4799,7 @@
"Would_you_like_to_return_the_queue": "Would you like to move back this room to the queue? All conversation history will be kept on the room.",
"Would_you_like_to_place_chat_on_hold": "Would you like to place this chat On-Hold?",
"Wrap_Up_the_Call": "Wrap Up the Call",
"Wrap_Up_Notes": "Wrap Up Notes",
"Yes": "Yes",
"Yes_archive_it": "Yes, archive it!",
"Yes_clear_all": "Yes, clear all!",
Expand Down
1 change: 1 addition & 0 deletions server/importPackages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,4 @@ import '../app/reactions/server';
import '../app/livechat/server';
import '../app/custom/server';
import '../app/authentication/server';
import '../app/voip/server/startup';
2 changes: 2 additions & 0 deletions server/sdk/types/IVoipService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export interface IVoipService {
checkManagementConnection(host: string, port: string, userName: string, password: string): Promise<IManagementServerConnectionStatus>;
checkCallserverConnection(websocketUrl: string, protocol?: string): Promise<IManagementServerConnectionStatus>;
cachedQueueDetails(): () => Promise<{ name: string; members: string[] }[]>;
init(): Promise<void>;
stop(): Promise<void>;
}
4 changes: 2 additions & 2 deletions server/services/omnichannel-voip/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
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()) / 1000;
return closedAt.getTime() - events[0].ts.getTime();
}

let currentOnHoldTime = 0;
Expand All @@ -284,7 +284,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn
const onHold = events[i].ts;
const unHold = events[i + 1]?.ts || closedAt;

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

return currentOnHoldTime;
Expand Down
Loading

0 comments on commit 44819b0

Please sign in to comment.