Skip to content

Commit 39634a0

Browse files
pierre-lehnen-rcggazzo
authored andcommitted
feat: External Call History (#40658)
1 parent b52cc06 commit 39634a0

43 files changed

Lines changed: 1713 additions & 25 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/meteor/client/views/mediaCallHistory/CallHistoryPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const getStateFilter = <T extends string[]>(states: T): T | [...T, 'error'] | un
4646
};
4747

4848
const getContact = (item: Serialized<CallHistoryItem>): CallHistoryContact => {
49-
if (item.external) {
49+
if (item.type !== 'media-call' || item.external) {
5050
return getExternalContact(item);
5151
}
5252

apps/meteor/client/views/mediaCallHistory/CallHistoryRowExternalUser.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ const CallHistoryRowExternalUser = ({ _id, contact, type, status, duration, time
2222
if (state === 'unavailable') {
2323
return [];
2424
}
25+
if (!('number' in contact) || !contact.number) {
26+
return [];
27+
}
28+
2529
const disabled = state !== 'available';
2630
return [
2731
{

apps/meteor/client/views/mediaCallHistory/MediaCallHistoryExternal.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ export const getExternalContact = (item: ExternalCallEndpointData['item']): Call
2525
};
2626
}
2727

28+
if (item.contactNumber) {
29+
return {
30+
number: item.contactNumber,
31+
name: item.contactName,
32+
};
33+
}
34+
35+
if (item.contactName) {
36+
return {
37+
name: item.contactName,
38+
};
39+
}
40+
2841
return { unknown: true };
2942
};
3043

@@ -50,6 +63,9 @@ const MediaCallHistoryExternal = ({ data, onClose }: MediaCallHistoryExternalPro
5063
if (state !== 'available') {
5164
return {};
5265
}
66+
if (!('number' in contact) || !contact.number) {
67+
return {};
68+
}
5369
return {
5470
voiceCall: () => toggleWidget(contact),
5571
};

apps/meteor/ee/server/settings/voip.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,47 @@ export function addSettings(): Promise<void> {
8282
invalidValue: 5060,
8383
});
8484
});
85+
86+
await this.section('VoIP_TeamCollab_ExternalCallHistory', async function () {
87+
await this.add('VoIP_TeamCollab_ExternalCallHistory_Enabled', false, {
88+
type: 'boolean',
89+
public: true,
90+
invalidValue: false,
91+
i18nDescription: 'VoIP_TeamCollab_ExternalCallHistory_Enabled_Description',
92+
});
93+
94+
const enableQuery = { _id: 'VoIP_TeamCollab_ExternalCallHistory_Enabled', value: true };
95+
96+
await this.add('VoIP_TeamCollab_ExternalCallHistory_Host', '', {
97+
type: 'string',
98+
public: false,
99+
invalidValue: '',
100+
enableQuery,
101+
});
102+
103+
await this.add('VoIP_TeamCollab_ExternalCallHistory_User', '', {
104+
type: 'string',
105+
public: false,
106+
invalidValue: '',
107+
enableQuery,
108+
});
109+
110+
await this.add('VoIP_TeamCollab_ExternalCallHistory_Password', '', {
111+
type: 'password',
112+
public: false,
113+
secret: true,
114+
invalidValue: '',
115+
enableQuery,
116+
});
117+
118+
await this.add('VoIP_TeamCollab_ExternalCallHistory_Timeout', 10000, {
119+
type: 'int',
120+
public: false,
121+
invalidValue: 10000,
122+
enableQuery,
123+
i18nDescription: 'VoIP_TeamCollab_ExternalCallHistory_Timeout_Description',
124+
});
125+
});
85126
},
86127
);
87128
});

apps/meteor/jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export default {
4444
'<rootDir>/app/utils/lib/**.spec.ts',
4545
'<rootDir>/server/lib/auditServerEvents/**.spec.ts',
4646
'<rootDir>/server/services/import/**/*.spec.ts',
47+
'<rootDir>/server/services/call-history/**/*.spec.ts',
4748
'<rootDir>/server/settings/lib/**.spec.ts',
4849
'<rootDir>/server/cron/**.spec.ts',
4950
'<rootDir>/app/api/server/**.spec.ts',
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Logger } from '@rocket.chat/logger';
2+
3+
export const logger = new Logger('CallHistory');
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export type MitelConfig = {
2+
host: string;
3+
username: string;
4+
password: string;
5+
timeout?: number;
6+
};
7+
8+
export type MitelCallItem = {
9+
directoryNumber?: string;
10+
name?: string;
11+
callIdentity?: string;
12+
dateTime: Date | null;
13+
timeZone?: string;
14+
duration: number;
15+
typeOfCall?: 'incoming-answered' | 'incoming-missed' | 'outgoing';
16+
transferredCall: boolean;
17+
divertedCall: boolean;
18+
firstDialledNumber?: string;
19+
remoteNumber?: string;
20+
directoryNumber2?: string;
21+
name2?: string;
22+
infoText2?: string;
23+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { serverFetch } from '@rocket.chat/server-fetch';
2+
3+
import type { MitelConfig } from './definition';
4+
import { logger } from '../logger';
5+
6+
export async function fetchMitelHistory(sipExtension: string, config: MitelConfig): Promise<string | null> {
7+
if (!sipExtension) {
8+
return null;
9+
}
10+
11+
const directoryNumber = sipExtension.replace(/\D/g, '');
12+
if (!directoryNumber) {
13+
logger.warn({ msg: "User's sip extension does not include numeric digits", sipExtension });
14+
return null;
15+
}
16+
17+
const separator = config.host.endsWith('/') ? '' : '/';
18+
const endpointUrl = `${config.host}${separator}callHistory/${directoryNumber}`;
19+
20+
let response: Awaited<ReturnType<typeof serverFetch>>;
21+
22+
try {
23+
logger.info({ msg: 'Fetching External call history', directoryNumber });
24+
response = await serverFetch(endpointUrl, {
25+
method: 'GET',
26+
// URL can only be configured by users with enough privileges, so it's fine to skip ssrf validation here
27+
ignoreSsrfValidation: true,
28+
auth: {
29+
type: 'digest',
30+
username: config.username,
31+
password: config.password,
32+
},
33+
});
34+
} catch (err) {
35+
logger.error({ msg: 'Failed to fetch External Call History', err });
36+
return null;
37+
}
38+
39+
if (response.status !== 200) {
40+
logger.error({
41+
msg: 'Failed to fetch External Call History',
42+
status: response.status,
43+
});
44+
45+
try {
46+
response.body?.resume();
47+
} catch {
48+
// ignore errors here
49+
}
50+
return null;
51+
}
52+
53+
try {
54+
return Buffer.from(await response.arrayBuffer()).toString('utf8');
55+
} catch (err) {
56+
logger.error({
57+
msg: 'Failed to read External Call History',
58+
err,
59+
});
60+
return null;
61+
}
62+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { IUser } from '@rocket.chat/core-typings';
2+
import { Users } from '@rocket.chat/models';
3+
4+
import type { MitelConfig } from './definition';
5+
import { fetchMitelHistory } from './fetchMitelHistory';
6+
import { importHistoryItems } from './importHistoryItems';
7+
import { logger } from '../logger';
8+
import { parseMitelJSON } from './parse/parseMitelJSON';
9+
10+
async function getUserDirectoryNumber(uid: IUser['_id']): Promise<string | null> {
11+
const user = await Users.findOneById<Pick<IUser, '_id' | 'freeSwitchExtension'>>(uid, { projection: { freeSwitchExtension: 1 } });
12+
return user?.freeSwitchExtension || null;
13+
}
14+
15+
async function processFetchedHistory(uid: IUser['_id'], result: string | null): Promise<void> {
16+
if (!result) {
17+
return;
18+
}
19+
20+
try {
21+
const callItems = parseMitelJSON(result);
22+
if (!callItems) {
23+
return;
24+
}
25+
26+
await importHistoryItems(uid, callItems);
27+
} catch (err) {
28+
logger.error({ msg: 'Unexpected error processing external call history', err });
29+
}
30+
}
31+
32+
async function fetchAndImportHistory(user: { directoryNumber: string; uid: IUser['_id'] }, config: MitelConfig): Promise<void> {
33+
const { timeout } = config;
34+
35+
if (!timeout) {
36+
const result = await fetchMitelHistory(user.directoryNumber, config);
37+
return processFetchedHistory(user.uid, result);
38+
}
39+
40+
// Set a timeout so that the request to the external service doesn't prevent our API request from working
41+
let promiseDecided = false;
42+
return new Promise((resolve, reject) => {
43+
fetchMitelHistory(user.directoryNumber, config)
44+
.then((result) => {
45+
const newPromise = processFetchedHistory(user.uid, result);
46+
47+
if (promiseDecided) {
48+
return;
49+
}
50+
51+
promiseDecided = true;
52+
// If the request completes successfully we can process its data, even if the timeout was already reached
53+
newPromise.then(resolve);
54+
})
55+
.catch((err) => {
56+
if (promiseDecided) {
57+
logger.error({ msg: 'Unexpected error on external call history processing', err });
58+
return;
59+
}
60+
promiseDecided = true;
61+
reject(err as Error);
62+
});
63+
64+
setTimeout(() => {
65+
if (promiseDecided) {
66+
return;
67+
}
68+
69+
promiseDecided = true;
70+
resolve();
71+
}, timeout);
72+
});
73+
}
74+
75+
export async function importHistoryForUser(uid: IUser['_id'], config: MitelConfig): Promise<void> {
76+
const directoryNumber = await getUserDirectoryNumber(uid);
77+
if (!directoryNumber) {
78+
return;
79+
}
80+
81+
return fetchAndImportHistory({ directoryNumber, uid }, config);
82+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { IUser } from '@rocket.chat/core-typings';
2+
import { CallHistory } from '@rocket.chat/models';
3+
4+
import { logger } from '../logger';
5+
import type { MitelCallItem } from './definition';
6+
import { convertMitelHistoryItem } from './parse/convertMitelHistoryItem';
7+
8+
export async function importHistoryItems(uid: IUser['_id'], items: MitelCallItem[]): Promise<void> {
9+
for (const item of items) {
10+
try {
11+
const record = convertMitelHistoryItem(item, uid);
12+
13+
await CallHistory.importHistoryItem(record);
14+
} catch (err) {
15+
logger.error({ msg: 'Failed to import Call History Item', uid, err });
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)