Skip to content

Commit 7925e9b

Browse files
chore: New Backend Architecture for Voice Calls (RocketChat#36718)
1 parent 24fba8d commit 7925e9b

38 files changed

Lines changed: 1830 additions & 173 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
diff --git a/lib/@types/index.d.ts b/lib/@types/index.d.ts
2+
index f71a82f458c1432202be8d4585fc70ba94bee4a4..b874ee9e1fa8a051f06d4824fc12161acfadb78a 100644
3+
--- a/lib/@types/index.d.ts
4+
+++ b/lib/@types/index.d.ts
5+
@@ -119,12 +119,12 @@ declare namespace Srf {
6+
on(messageType: "modify", callback: (req: SrfRequest, res: SrfResponse) => void): void;
7+
once(messageType: string, callback: (msg: SrfResponse) => void): void;
8+
listeners(messageType: string): any[];
9+
- request(opts?: SrfRequest): Promise<SrfResponse>;
10+
- request(opts: SrfRequest, callback?: (err: any, msg: SrfResponse) => void): void;
11+
+ request(opts?: Partial<SrfRequest>): Promise<SrfResponse>;
12+
+ request(opts: Partial<SrfRequest>, callback?: (err: any, msg: SrfResponse) => void): void;
13+
}
14+
15+
export interface CreateUASOptions {
16+
- localSdp: string;
17+
+ localSdp: string | (() => string | Promise<string>);
18+
headers?: SipMessageHeaders;
19+
}
20+
21+
@@ -135,6 +135,8 @@ declare namespace Srf {
22+
localSdp?: string;
23+
proxy?: string;
24+
auth?: { username: string; password: string; };
25+
+ callingName?: string;
26+
+ callingNumber?: string;
27+
}
28+
29+
export interface CreateB2BUAOptions {
30+
@@ -155,7 +157,7 @@ declare class Srf extends EventEmitter {
31+
constructor();
32+
constructor(tags: string | string[]);
33+
connect(config?: Srf.SrfConfig): Promise<void>;
34+
- disconnect(): void;
35+
+ disconnect(socket?: Socket): void;
36+
use(callback: (req: Srf.SrfRequest, res: Srf.SrfResponse, next: Function) => void): void;
37+
use(messageType: string, callback: (req: Srf.SrfRequest, res: Srf.SrfResponse, next: Function) => void): void;
38+
invite(callback: (req: Srf.SrfRequest, res: Srf.SrfResponse) => void): void;
39+
@@ -164,12 +166,12 @@ declare class Srf extends EventEmitter {
40+
proxyRequest(req: Srf.SrfRequest, destination: string | string[], opts?: Srf.ProxyRequestOptions, callback?: (err: any, results: string) => void): void;
41+
createUAS(req: Srf.SrfRequest, res: Srf.SrfResponse, opts: Srf.CreateUASOptions): Promise<Srf.Dialog>;
42+
createUAS(req: Srf.SrfRequest, res: Srf.SrfResponse, opts: Srf.CreateUASOptions, callback?: (err: any, dialog: Srf.Dialog) => void): this;
43+
- createUAC(uri: string | Srf.CreateUACOptions, opts?: Srf.CreateUACOptions, progressCallbacks?: { cbRequest?: (req: Srf.SrfRequest) => void; cbProvisional?: (provisionalRes: Srf.SrfResponse) => void; }): Promise<Srf.Dialog>;
44+
- createUAC(uri: string | Srf.CreateUACOptions, opts?: Srf.CreateUACOptions, progressCallbacks?: { cbRequest?: (req: Srf.SrfRequest) => void; cbProvisional?: (provisionalRes: Srf.SrfResponse) => void; }, callback?: (err: any, dialog: Srf.Dialog) => void): this;
45+
- createB2BUA(req: Srf.SrfRequest, res: Srf.SrfResponse, uri: string, opts: Srf.CreateB2BUAOptions, progressCallbacks?: { cbRequest?: (req: Srf.SrfRequest) => void; cbProvisional?: (provisionalRes: Response) => void; cbFinalizedUac?: (uac: Srf.Dialog) => void; }): Promise<{ uas: Srf.Dialog; uac: Srf.Dialog }>;
46+
- createB2BUA(req: Srf.SrfRequest, res: Srf.SrfResponse, uri: string, opts: Srf.CreateB2BUAOptions, progressCallbacks?: { cbRequest?: (req: Srf.SrfRequest) => void; cbProvisional?: (provisionalRes: Response) => void; cbFinalizedUac?: (uac: Srf.Dialog) => void; }, callback?: (err: any, dialog: Srf.Dialog) => void): this;
47+
+ createUAC(uri: string | Srf.CreateUACOptions, opts?: Srf.CreateUACOptions, progressCallbacks?: { cbRequest?: (error: unknown, req: Srf.SrfRequest) => void; cbProvisional?: (provisionalRes: Srf.SrfResponse) => void; }): Promise<Srf.Dialog>;
48+
+ createUAC(uri: string | Srf.CreateUACOptions, opts?: Srf.CreateUACOptions, progressCallbacks?: { cbRequest?: (error: unknown, req: Srf.SrfRequest) => void; cbProvisional?: (provisionalRes: Srf.SrfResponse) => void; }, callback?: (err: any, dialog: Srf.Dialog) => void): this;
49+
+ createB2BUA(req: Srf.SrfRequest, res: Srf.SrfResponse, uri: string, opts: Srf.CreateB2BUAOptions, progressCallbacks?: { cbRequest?: (error: unknown, req: Srf.SrfRequest) => void; cbProvisional?: (provisionalRes: Response) => void; cbFinalizedUac?: (uac: Srf.Dialog) => void; }): Promise<{ uas: Srf.Dialog; uac: Srf.Dialog }>;
50+
+ createB2BUA(req: Srf.SrfRequest, res: Srf.SrfResponse, uri: string, opts: Srf.CreateB2BUAOptions, progressCallbacks?: { cbRequest?: (error: unknown, req: Srf.SrfRequest) => void; cbProvisional?: (provisionalRes: Response) => void; cbFinalizedUac?: (uac: Srf.Dialog) => void; }, callback?: (err: any, dialog: Srf.Dialog) => void): this;
51+
on(event: 'connect', listener: (err: Error, hostPort: string) => void): this;
52+
- on(event: 'error', listener: (err: Error) => void): this;
53+
+ on(event: 'error', listener: (err: Error, socket?: Socket) => void): this;
54+
on(event: 'disconnect', listener: () => void): this;
55+
on(event: 'message', listener: (req: Srf.SrfRequest, res: Srf.SrfResponse) => void): this;
56+
on(event: 'request', listener: (req: Srf.SrfRequest, res: Srf.SrfResponse) => void): this;

apps/meteor/app/authorization/server/constant/permissions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ export const permissions = [
228228
// Allow viewing details of an extension
229229
{ _id: 'view-voip-extension-details', roles: ['admin', 'user'] },
230230

231+
// New Media calls permissions
232+
{ _id: 'allow-internal-voice-calls', roles: ['admin', 'user'] },
233+
{ _id: 'allow-external-voice-calls', roles: ['admin', 'user'] },
234+
231235
{ _id: 'remove-livechat-department', roles: ['livechat-manager', 'admin'] },
232236
{ _id: 'manage-apps', roles: ['admin'] },
233237
{ _id: 'post-readonly', roles: ['admin', 'owner', 'moderator'] },

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

Lines changed: 92 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,53 +17,106 @@ export function addSettings(): Promise<void> {
1717
alert: 'VoIP_TeamCollab_Beta_Alert',
1818
});
1919

20-
await this.add('VoIP_TeamCollab_FreeSwitch_Host', '', {
21-
type: 'string',
22-
public: true,
23-
invalidValue: '',
24-
enableQuery,
25-
});
20+
await this.section('VoIP_TeamCollab_WebRTC', async function () {
21+
await this.add('VoIP_TeamCollab_Ice_Servers', 'stun:stun.l.google.com:19302', {
22+
type: 'string',
23+
public: true,
24+
invalidValue: '',
25+
enableQuery,
26+
});
2627

27-
await this.add('VoIP_TeamCollab_FreeSwitch_Port', 8021, {
28-
type: 'int',
29-
public: true,
30-
invalidValue: 8021,
31-
enableQuery,
28+
await this.add('VoIP_TeamCollab_Ice_Gathering_Timeout', 5000, {
29+
type: 'int',
30+
public: true,
31+
invalidValue: 5000,
32+
enableQuery,
33+
});
3234
});
3335

34-
await this.add('VoIP_TeamCollab_FreeSwitch_Password', '', {
35-
type: 'password',
36-
secret: true,
37-
invalidValue: '',
38-
enableQuery,
39-
});
36+
await this.section('VoIP_TeamCollab_SIP_Integration', async function () {
37+
await this.add('VoIP_TeamCollab_SIP_Integration_Enabled', false, {
38+
type: 'boolean',
39+
public: true,
40+
invalidValue: false,
41+
});
4042

41-
await this.add('VoIP_TeamCollab_FreeSwitch_Timeout', 3000, {
42-
type: 'int',
43-
public: true,
44-
invalidValue: 3000,
45-
enableQuery,
46-
});
43+
await this.add('VoIP_TeamCollab_SIP_Integration_For_Internal_Calls', false, {
44+
type: 'boolean',
45+
public: true,
46+
invalidValue: false,
47+
});
4748

48-
await this.add('VoIP_TeamCollab_FreeSwitch_WebSocket_Path', '', {
49-
type: 'string',
50-
public: true,
51-
invalidValue: '',
52-
enableQuery,
53-
});
49+
await this.add('VoIP_TeamCollab_Drachtio_Host', '', {
50+
type: 'string',
51+
public: false,
52+
invalidValue: '',
53+
enableQuery,
54+
});
5455

55-
await this.add('VoIP_TeamCollab_Ice_Servers', 'stun:stun.l.google.com:19302', {
56-
type: 'string',
57-
public: true,
58-
invalidValue: '',
59-
enableQuery,
56+
await this.add('VoIP_TeamCollab_Drachtio_Port', 9022, {
57+
type: 'int',
58+
public: false,
59+
invalidValue: 9022,
60+
enableQuery,
61+
});
62+
63+
await this.add('VoIP_TeamCollab_Drachtio_Password', '', {
64+
type: 'password',
65+
secret: true,
66+
invalidValue: '',
67+
enableQuery,
68+
});
69+
70+
await this.add('VoIP_TeamCollab_SIP_Server_Host', '', {
71+
type: 'string',
72+
public: false,
73+
invalidValue: '',
74+
enableQuery,
75+
});
76+
77+
await this.add('VoIP_TeamCollab_SIP_Server_Port', 5080, {
78+
type: 'int',
79+
public: false,
80+
invalidValue: 5080,
81+
enableQuery,
82+
});
6083
});
6184

62-
await this.add('VoIP_TeamCollab_Ice_Gathering_Timeout', 5000, {
63-
type: 'int',
64-
public: true,
65-
invalidValue: 5000,
66-
enableQuery,
85+
await this.section('VoIP_TeamCollab_FreeSwitch', async function () {
86+
await this.add('VoIP_TeamCollab_FreeSwitch_Host', '', {
87+
type: 'string',
88+
public: false,
89+
invalidValue: '',
90+
enableQuery,
91+
});
92+
93+
await this.add('VoIP_TeamCollab_FreeSwitch_Port', 8021, {
94+
type: 'int',
95+
public: false,
96+
invalidValue: 8021,
97+
enableQuery,
98+
});
99+
100+
await this.add('VoIP_TeamCollab_FreeSwitch_Password', '', {
101+
type: 'password',
102+
secret: true,
103+
invalidValue: '',
104+
enableQuery,
105+
});
106+
107+
await this.add('VoIP_TeamCollab_FreeSwitch_Timeout', 3000, {
108+
type: 'int',
109+
public: true,
110+
invalidValue: 3000,
111+
enableQuery,
112+
});
113+
114+
await this.add('VoIP_TeamCollab_FreeSwitch_WebSocket_Path', '', {
115+
type: 'string',
116+
public: true,
117+
invalidValue: '',
118+
enableQuery,
119+
});
67120
});
68121
},
69122
);

apps/meteor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@
345345
"date.js": "~0.3.3",
346346
"debug": "~4.3.7",
347347
"dompurify": "^3.2.6",
348+
"drachtio-srf": "patch:drachtio-srf@npm%3A5.0.12#~/.yarn/patches/drachtio-srf-npm-5.0.12-b0b1afaad6.patch",
348349
"ejson": "^2.2.3",
349350
"emailreplyparser": "^0.0.5",
350351
"emoji-toolkit": "^7.0.1",

apps/meteor/server/services/media-call/service.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { api, ServiceClassInternal, type IMediaCallService } from '@rocket.chat/core-services';
1+
import { api, ServiceClassInternal, type IMediaCallService, Authorization } from '@rocket.chat/core-services';
22
import type { IUser } from '@rocket.chat/core-typings';
33
import { Logger } from '@rocket.chat/logger';
44
import { callServer, type IMediaCallServerSettings } from '@rocket.chat/media-calls';
@@ -15,6 +15,8 @@ export class MediaCallService extends ServiceClassInternal implements IMediaCall
1515
constructor() {
1616
super();
1717
callServer.emitter.on('signalRequest', ({ toUid, signal }) => this.sendSignal(toUid, signal));
18+
callServer.emitter.on('callUpdated', (params) => api.broadcast('media-call.updated', params));
19+
this.onEvent('media-call.updated', (params) => callServer.receiveCallUpdate(params));
1820

1921
this.onEvent('watch.settings', async ({ setting }): Promise<void> => {
2022
if (setting._id.startsWith('VoIP_TeamCollab_')) {
@@ -70,8 +72,8 @@ export class MediaCallService extends ServiceClassInternal implements IMediaCall
7072

7173
private getMediaServerSettings(): IMediaCallServerSettings {
7274
const enabled = settings.get<boolean>('VoIP_TeamCollab_Enabled') ?? false;
73-
const sipEnabled = false;
74-
const forceSip = false;
75+
const sipEnabled = enabled && (settings.get<boolean>('VoIP_TeamCollab_SIP_Integration_Enabled') ?? false);
76+
const forceSip = sipEnabled && (settings.get<boolean>('VoIP_TeamCollab_SIP_Integration_For_Internal_Calls') ?? false);
7577

7678
return {
7779
enabled,
@@ -81,10 +83,30 @@ export class MediaCallService extends ServiceClassInternal implements IMediaCall
8183
},
8284
sip: {
8385
enabled: sipEnabled,
86+
drachtio: {
87+
host: settings.get<string>('VoIP_TeamCollab_Drachtio_Host') ?? '',
88+
port: settings.get<number>('VoIP_TeamCollab_Drachtio_Port') ?? 9022,
89+
secret: settings.get<string>('VoIP_TeamCollab_Drachtio_Password') ?? '',
90+
},
91+
sipServer: {
92+
host: settings.get<string>('VoIP_TeamCollab_SIP_Server_Host') ?? '',
93+
port: settings.get<number>('VoIP_TeamCollab_SIP_Server_Port') ?? 5080,
94+
},
8495
},
96+
permissionCheck: (uid, callType) => this.userHasMediaCallPermission(uid, callType),
8597
};
8698
}
8799

100+
private async userHasMediaCallPermission(uid: IUser['_id'], callType: 'internal' | 'external' | 'any'): Promise<boolean> {
101+
if (callType === 'any') {
102+
return Authorization.hasAtLeastOnePermission(uid, ['allow-internal-voice-calls', 'allow-external-voice-calls']);
103+
}
104+
105+
const permissionId = `allow-${callType}-voice-calls`;
106+
107+
return Authorization.hasPermission(uid, permissionId);
108+
}
109+
88110
private async deserializeClientSignal(serialized: string): Promise<ClientMediaSignal> {
89111
try {
90112
const signal = JSON.parse(serialized);

ee/packages/media-calls/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@rocket.chat/emitter": "~0.31.25",
2929
"@rocket.chat/logger": "workspace:^",
3030
"@rocket.chat/media-signaling": "workspace:^",
31-
"@rocket.chat/models": "workspace:^"
31+
"@rocket.chat/models": "workspace:^",
32+
"drachtio-srf": "patch:drachtio-srf@npm%3A5.0.12#~/.yarn/patches/drachtio-srf-npm-5.0.12-b0b1afaad6.patch"
3233
}
3334
}

ee/packages/media-calls/src/base/BaseAgent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ export abstract class BaseMediaCallAgent implements IMediaCallAgent {
7777

7878
public abstract onCallTransferred(callId: string): Promise<void>;
7979

80+
public abstract onDTMF(callId: string, dtmf: string, duration: number): Promise<void>;
81+
8082
protected async createOrUpdateChannel(call: IMediaCall, contractId: string): Promise<IMediaCallChannel> {
8183
if (!contractId) {
8284
throw new Error('error-invalid-contract');
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import type { IMediaCall } from '@rocket.chat/core-typings';
2+
import type { ClientMediaSignalBody } from '@rocket.chat/media-signaling';
3+
4+
import { logger } from '../logger';
25

36
export class BaseCallProvider {
47
public get callId(): string {
58
return this.call._id;
69
}
710

811
constructor(public readonly call: IMediaCall) {}
12+
13+
public async reactToCallChanges(params: { dtmf?: ClientMediaSignalBody<'dtmf'> }): Promise<void> {
14+
logger.debug({ msg: 'BaseCallProvider.reactToCallChanges', callId: this.callId, params });
15+
}
916
}

ee/packages/media-calls/src/definition/IMediaCallAgent.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IMediaCall, MediaCallActor, MediaCallActorType } from '@rocket.chat/core-typings';
1+
import type { IMediaCall, MediaCallActor, MediaCallActorType, MediaCallContact } from '@rocket.chat/core-typings';
22
import type { CallRole } from '@rocket.chat/media-signaling';
33

44
export interface IMediaCallAgent {
@@ -18,7 +18,9 @@ export interface IMediaCallAgent {
1818
/* Called when the sdp of the other actor is available, regardless of call state, or when this actor must provide an offer */
1919
onRemoteDescriptionChanged(callId: string, negotiationId: string): Promise<void>;
2020

21+
onDTMF(callId: string, tone: string, duration: number): Promise<void>;
22+
2123
onCallTransferred(callId: string): Promise<void>;
2224

23-
getMyCallActor(call: IMediaCall): MediaCallActor;
25+
getMyCallActor(call: IMediaCall): MediaCallContact;
2426
}

ee/packages/media-calls/src/definition/IMediaCallServer.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { IUser } from '@rocket.chat/core-typings';
22
import type { Emitter } from '@rocket.chat/emitter';
3-
import type { ClientMediaSignal, ServerMediaSignal } from '@rocket.chat/media-signaling';
3+
import type { ClientMediaSignal, ClientMediaSignalBody, ServerMediaSignal } from '@rocket.chat/media-signaling';
44

55
import type { InternalCallParams } from './common';
66

77
export type MediaCallServerEvents = {
8+
callUpdated: { callId: string; dtmf?: ClientMediaSignalBody<'dtmf'> };
89
signalRequest: { toUid: IUser['_id']; signal: ServerMediaSignal };
910
};
1011

@@ -18,17 +19,30 @@ export interface IMediaCallServerSettings {
1819

1920
sip: {
2021
enabled: boolean;
22+
drachtio: {
23+
host: string;
24+
port: number;
25+
secret: string;
26+
};
27+
sipServer: {
28+
host: string;
29+
port: number;
30+
};
2131
};
32+
33+
permissionCheck: (uid: IUser['_id'], callType: 'internal' | 'external' | 'any') => Promise<boolean>;
2234
}
2335

2436
export interface IMediaCallServer {
2537
emitter: Emitter<MediaCallServerEvents>;
2638

2739
// functions that trigger events
2840
sendSignal(toUid: IUser['_id'], signal: ServerMediaSignal): void;
41+
reportCallUpdate(params: { callId: string; dtmf?: ClientMediaSignalBody<'dtmf'> }): void;
2942

3043
// functions that are run on events
3144
receiveSignal(fromUid: IUser['_id'], signal: ClientMediaSignal): void;
45+
receiveCallUpdate(params: { callId: string; dtmf?: ClientMediaSignalBody<'dtmf'> }): void;
3246

3347
// extra functions available to the service
3448
hangupExpiredCalls(): Promise<void>;

0 commit comments

Comments
 (0)