Skip to content

Commit 93fef88

Browse files
fix: Re-establish connection with FreeSwitch server when it is lost (#36230)
1 parent 15baefa commit 93fef88

File tree

16 files changed

+401
-199
lines changed

16 files changed

+401
-199
lines changed

.changeset/great-actors-double.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/freeswitch': patch
3+
'@rocket.chat/meteor': patch
4+
---
5+
6+
Fixes FreeSwitch event parser to automatically reconnect when connection is lost

apps/meteor/ee/server/local-services/voip-freeswitch/service.ts

Lines changed: 120 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ import type {
1111
AtLeast,
1212
} from '@rocket.chat/core-typings';
1313
import { isKnownFreeSwitchEventType } from '@rocket.chat/core-typings';
14-
import { getDomain, getUserPassword, getExtensionList, getExtensionDetails, listenToEvents } from '@rocket.chat/freeswitch';
14+
import {
15+
getDomain,
16+
getUserPassword,
17+
getExtensionList,
18+
getExtensionDetails,
19+
FreeSwitchEventClient,
20+
type FreeSwitchOptions,
21+
} from '@rocket.chat/freeswitch';
1522
import type { InsertionModel } from '@rocket.chat/model-typings';
1623
import { FreeSwitchCall, FreeSwitchEvent, Users } from '@rocket.chat/models';
1724
import { objectMap, wrapExceptions } from '@rocket.chat/tools';
@@ -25,47 +32,135 @@ export class VoipFreeSwitchService extends ServiceClassInternal implements IVoip
2532

2633
private serviceStarter: ServiceStarter;
2734

35+
private eventClient: FreeSwitchEventClient | null = null;
36+
37+
private wasEverConnected = false;
38+
2839
constructor() {
2940
super();
3041

31-
this.serviceStarter = new ServiceStarter(() => this.startEvents());
42+
this.serviceStarter = new ServiceStarter(
43+
async () => {
44+
// Delay start to ensure setting values are up-to-date in the cache
45+
setImmediate(() => this.startEvents());
46+
},
47+
async () => this.stopEvents(),
48+
);
3249
this.onEvent('watch.settings', async ({ setting }): Promise<void> => {
33-
if (setting._id === 'VoIP_TeamCollab_Enabled' && setting.value === true) {
34-
void this.serviceStarter.start();
50+
if (setting._id === 'VoIP_TeamCollab_Enabled') {
51+
if (setting.value !== true) {
52+
void this.serviceStarter.stop();
53+
return;
54+
}
55+
56+
if (setting.value === true) {
57+
void this.serviceStarter.start();
58+
return;
59+
}
60+
}
61+
62+
if (setting._id === 'VoIP_TeamCollab_FreeSwitch_Host') {
63+
// Re-connect if the host changes
64+
if (this.eventClient && this.eventClient.host !== setting.value) {
65+
this.stopEvents();
66+
}
67+
68+
if (setting.value) {
69+
void this.serviceStarter.start();
70+
}
71+
}
72+
73+
// If any other freeswitch setting changes, only reconnect if it's not yet connected
74+
if (setting._id.startsWith('VoIP_TeamCollab_FreeSwitch_')) {
75+
if (!this.eventClient?.isReady()) {
76+
this.stopEvents();
77+
void this.serviceStarter.start();
78+
}
3579
}
3680
});
3781
}
3882

39-
private listening = false;
40-
4183
public async started(): Promise<void> {
4284
void this.serviceStarter.start();
4385
}
4486

4587
private async startEvents(): Promise<void> {
46-
if (this.listening) {
88+
if (this.eventClient) {
89+
if (!this.eventClient.isDone()) {
90+
return;
91+
}
92+
93+
const client = this.eventClient;
94+
this.eventClient = null;
95+
client.endConnection();
96+
}
97+
98+
const options = wrapExceptions(() => this.getConnectionSettings()).suppress();
99+
if (!options) {
100+
this.wasEverConnected = false;
47101
return;
48102
}
49103

50-
try {
51-
// #ToDo: Reconnection
52-
// #ToDo: Only connect from one rocket.chat instance
53-
await listenToEvents(
54-
async (...args) => wrapExceptions(() => this.onFreeSwitchEvent(...args)).suppress(),
55-
this.getConnectionSettings(),
56-
);
57-
this.listening = true;
58-
} catch (_e) {
59-
this.listening = false;
104+
this.initializeEventClient(options);
105+
}
106+
107+
private retryEventsLater(): void {
108+
// Try to re-establish connection after some time
109+
setTimeout(
110+
() => {
111+
void this.startEvents();
112+
},
113+
this.wasEverConnected ? 3000 : 20_000,
114+
);
115+
}
116+
117+
private initializeEventClient(options: FreeSwitchOptions): void {
118+
const client = FreeSwitchEventClient.listenToEvents(options);
119+
this.eventClient = client;
120+
121+
client.on('ready', () => {
122+
if (this.eventClient !== client) {
123+
return;
124+
}
125+
this.wasEverConnected = true;
126+
});
127+
128+
client.on('end', () => {
129+
if (this.eventClient && this.eventClient !== client) {
130+
return;
131+
}
132+
133+
this.eventClient = null;
134+
this.retryEventsLater();
135+
});
136+
137+
client.on('event', async ({ eventName, eventData }) => {
138+
if (this.eventClient !== client) {
139+
return;
140+
}
141+
142+
await wrapExceptions(() =>
143+
this.onFreeSwitchEvent(eventName as string, eventData as unknown as Record<string, string | undefined>),
144+
).suppress();
145+
});
146+
}
147+
148+
private stopEvents(): void {
149+
if (!this.eventClient) {
150+
return;
60151
}
152+
153+
this.eventClient.endConnection();
154+
this.wasEverConnected = false;
155+
this.eventClient = null;
61156
}
62157

63-
private getConnectionSettings(): { host: string; port: number; password: string; timeout: number } {
64-
if (!settings.get('VoIP_TeamCollab_Enabled') && !process.env.FREESWITCHIP) {
158+
private getConnectionSettings(): FreeSwitchOptions {
159+
if (!settings.get('VoIP_TeamCollab_Enabled')) {
65160
throw new Error('VoIP is disabled.');
66161
}
67162

68-
const host = process.env.FREESWITCHIP || settings.get<string>('VoIP_TeamCollab_FreeSwitch_Host');
163+
const host = settings.get<string>('VoIP_TeamCollab_FreeSwitch_Host');
69164
if (!host) {
70165
throw new Error('VoIP is not properly configured.');
71166
}
@@ -75,14 +170,16 @@ export class VoipFreeSwitchService extends ServiceClassInternal implements IVoip
75170
const password = settings.get<string>('VoIP_TeamCollab_FreeSwitch_Password');
76171

77172
return {
78-
host,
79-
port,
173+
socketOptions: {
174+
host,
175+
port,
176+
},
80177
password,
81178
timeout,
82179
};
83180
}
84181

85-
private async onFreeSwitchEvent(eventName: string, data: Record<string, string | undefined>): Promise<void> {
182+
public async onFreeSwitchEvent(eventName: string, data: Record<string, string | undefined>): Promise<void> {
86183
const uniqueId = data['Unique-ID'];
87184
if (!uniqueId) {
88185
return;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export type FreeSwitchOptions = { host?: string; port?: number; password?: string; timeout?: number };
1+
export type FreeSwitchOptions = { socketOptions: { host: string; port: number }; password: string; timeout?: number };

packages/freeswitch/src/commands/getDomain.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { StringMap } from 'esl';
22

33
import type { FreeSwitchOptions } from '../FreeSwitchOptions';
4+
import { FreeSwitchApiClient } from '../esl';
45
import { logger } from '../logger';
5-
import { runCommand } from '../runCommand';
66

77
export function getCommandGetDomain(): string {
88
return 'eval ${domain}';
@@ -20,6 +20,6 @@ export function parseDomainResponse(response: StringMap): string {
2020
}
2121

2222
export async function getDomain(options: FreeSwitchOptions): Promise<string> {
23-
const response = await runCommand(options, getCommandGetDomain());
23+
const response = await FreeSwitchApiClient.runSingleCommand(options, getCommandGetDomain());
2424
return parseDomainResponse(response);
2525
}

packages/freeswitch/src/commands/getExtensionDetails.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FreeSwitchExtension } from '@rocket.chat/core-typings';
22

33
import type { FreeSwitchOptions } from '../FreeSwitchOptions';
4-
import { runCommand } from '../runCommand';
4+
import { FreeSwitchApiClient } from '../esl';
55
import { mapUserData } from '../utils/mapUserData';
66
import { parseUserList } from '../utils/parseUserList';
77

@@ -14,7 +14,7 @@ export async function getExtensionDetails(
1414
requestParams: { extension: string; group?: string },
1515
): Promise<FreeSwitchExtension> {
1616
const { extension, group } = requestParams;
17-
const response = await runCommand(options, getCommandListFilteredUser(extension, group));
17+
const response = await FreeSwitchApiClient.runSingleCommand(options, getCommandListFilteredUser(extension, group));
1818

1919
const users = parseUserList(response);
2020

packages/freeswitch/src/commands/getExtensionList.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FreeSwitchExtension } from '@rocket.chat/core-typings';
22

33
import type { FreeSwitchOptions } from '../FreeSwitchOptions';
4-
import { runCommand } from '../runCommand';
4+
import { FreeSwitchApiClient } from '../esl';
55
import { mapUserData } from '../utils/mapUserData';
66
import { parseUserList } from '../utils/parseUserList';
77

@@ -10,7 +10,7 @@ export function getCommandListUsers(): string {
1010
}
1111

1212
export async function getExtensionList(options: FreeSwitchOptions): Promise<FreeSwitchExtension[]> {
13-
const response = await runCommand(options, getCommandListUsers());
13+
const response = await FreeSwitchApiClient.runSingleCommand(options, getCommandListUsers());
1414
const users = parseUserList(response);
1515

1616
return users.map((item) => mapUserData(item));

packages/freeswitch/src/commands/getUserPassword.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { StringMap } from 'esl';
22

33
import type { FreeSwitchOptions } from '../FreeSwitchOptions';
44
import { logger } from '../logger';
5-
import { runCallback } from '../runCommand';
65
import { getCommandGetDomain, parseDomainResponse } from './getDomain';
6+
import { FreeSwitchApiClient } from '../esl';
77

88
export function getCommandGetUserPassword(user: string, domain = 'rocket.chat'): string {
99
return `user_data ${user}@${domain} param password`;
@@ -21,7 +21,7 @@ export function parsePasswordResponse(response: StringMap): string {
2121
}
2222

2323
export async function getUserPassword(options: FreeSwitchOptions, user: string): Promise<string> {
24-
return runCallback(options, async (runCommand) => {
24+
return FreeSwitchApiClient.runCallback(options, async (runCommand) => {
2525
const domainResponse = await runCommand(getCommandGetDomain());
2626
const domain = parseDomainResponse(domainResponse);
2727

packages/freeswitch/src/connect.ts

Lines changed: 0 additions & 87 deletions
This file was deleted.

0 commit comments

Comments
 (0)