feat: Integrate Wavoip for voice call functionality

- Added Wavoip integration to support voice calls within the application.
- Introduced `wavoipToken` in various DTOs and models to manage authentication.
- Updated `ChannelStartupService` to handle Wavoip settings and events.
- Enhanced `BaileysStartupService` to utilize Wavoip for call signaling.
- Updated CHANGELOG.md to version 1.8.7.
This commit is contained in:
Davidson Gomes 2025-05-29 19:20:09 -03:00
parent 8e65526ce9
commit 5043ce8405
12 changed files with 340 additions and 24 deletions

View File

@ -1,4 +1,10 @@
# 1.8.6 (develop)
# 1.8.7
### Features
* Wavoip integration
# 1.8.6
### Features

View File

@ -84,6 +84,7 @@
"redis": "^4.6.5",
"sharp": "^0.32.2",
"socket.io": "^4.7.1",
"socket.io-client": "^4.8.1",
"socks-proxy-agent": "^8.0.1",
"swagger-ui-express": "^5.0.0",
"uuid": "^9.0.0",

View File

@ -78,6 +78,7 @@ export class InstanceController {
read_messages,
read_status,
sync_full_history,
wavoipToken,
websocket_enabled,
websocket_events,
rabbitmq_enabled,
@ -401,6 +402,7 @@ export class InstanceController {
read_messages: read_messages || false,
read_status: read_status || false,
sync_full_history: sync_full_history ?? false,
wavoipToken: wavoipToken ?? '',
};
this.logger.verbose('settings: ' + JSON.stringify(settings));

View File

@ -21,6 +21,7 @@ export class InstanceDto {
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
wavoipToken?: string;
chatwoot_account_id?: string;
chatwoot_token?: string;
chatwoot_url?: string;

View File

@ -6,4 +6,5 @@ export class SettingsDto {
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
wavoipToken?: string;
}

View File

@ -11,6 +11,7 @@ export class SettingsRaw {
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
wavoipToken?: string;
}
const settingsSchema = new Schema<SettingsRaw>({
@ -22,6 +23,7 @@ const settingsSchema = new Schema<SettingsRaw>({
read_messages: { type: Boolean, required: true },
read_status: { type: Boolean, required: true },
sync_full_history: { type: Boolean, required: true },
wavoipToken: { type: String, required: true },
});
export const SettingsModel = dbserver?.model(SettingsRaw.name, settingsSchema, 'settings');

View File

@ -181,6 +181,9 @@ export class ChannelStartupService {
this.localSettings.sync_full_history = data?.sync_full_history;
this.logger.verbose(`Settings sync_full_history: ${this.localSettings.sync_full_history}`);
this.localSettings.wavoipToken = data?.wavoipToken;
this.logger.verbose(`Settings wavoipToken: ${this.localSettings.wavoipToken}`);
this.logger.verbose('Settings loaded');
}
@ -194,6 +197,7 @@ export class ChannelStartupService {
this.logger.verbose(`Settings read_messages: ${data.read_messages}`);
this.logger.verbose(`Settings read_status: ${data.read_status}`);
this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`);
this.logger.verbose(`Settings wavoipToken: ${data.wavoipToken}`);
Object.assign(this.localSettings, data);
this.logger.verbose('Settings set');
}
@ -214,6 +218,7 @@ export class ChannelStartupService {
this.logger.verbose(`Settings read_messages: ${data.read_messages}`);
this.logger.verbose(`Settings read_status: ${data.read_status}`);
this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`);
this.logger.verbose(`Settings wavoipToken: ${data.wavoipToken}`);
return {
reject_call: data.reject_call,
msg_call: data.msg_call,
@ -222,6 +227,7 @@ export class ChannelStartupService {
read_messages: data.read_messages,
read_status: data.read_status,
sync_full_history: data.sync_full_history,
wavoipToken: data.wavoipToken,
};
}
@ -719,7 +725,12 @@ export class ChannelStartupService {
}
}
public async sendDataWebhook<T = any>(event: Events, data: T, local = true) {
public async sendDataWebhook<T = any>(
event: Events,
data: T,
local = true,
integration = ['websocket', 'rabbitmq', 'sqs', 'webhook'],
) {
const webhookGlobal = this.configService.get<Webhook>('WEBHOOK');
const webhookLocal = this.localWebhook.events;
const websocketLocal = this.localWebsocket.events;
@ -739,7 +750,7 @@ export class ChannelStartupService {
const tokenStore = await this.repository.auth.find(this.instanceName);
const instanceApikey = tokenStore?.apikey || 'Apikey not found';
if (rabbitmqEnabled) {
if (rabbitmqEnabled && integration.includes('rabbitmq')) {
const amqp = getAMQP();
if (this.localRabbitmq.enabled && amqp) {
if (Array.isArray(rabbitmqLocal) && rabbitmqLocal.includes(we)) {
@ -884,7 +895,7 @@ export class ChannelStartupService {
}
}
if (this.localSqs.enabled) {
if (this.localSqs.enabled && integration.includes('sqs')) {
const sqs = getSQS();
if (sqs) {
@ -954,7 +965,7 @@ export class ChannelStartupService {
}
}
if (this.configService.get<Websocket>('WEBSOCKET')?.ENABLED) {
if (this.configService.get<Websocket>('WEBSOCKET')?.ENABLED && integration.includes('websocket')) {
this.logger.verbose('Sending data to websocket on channel: ' + this.instance.name);
const io = getIO();
@ -1028,7 +1039,7 @@ export class ChannelStartupService {
const globalApiKey = this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;
if (local) {
if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) {
if (Array.isArray(webhookLocal) && webhookLocal.includes(we) && integration.includes('webhook')) {
this.logger.verbose('Sending data to webhook local');
let baseURL: string;
@ -1096,7 +1107,7 @@ export class ChannelStartupService {
}
}
if (webhookGlobal.GLOBAL?.ENABLED) {
if (webhookGlobal.GLOBAL?.ENABLED && integration.includes('webhook')) {
if (webhookGlobal.EVENTS[we]) {
this.logger.verbose('Sending data to webhook global');
const globalWebhook = this.configService.get<Webhook>('WEBHOOK').GLOBAL;

View File

@ -0,0 +1,78 @@
import { BinaryNode, Contact, JidWithDevice, proto, WAConnectionState } from 'baileys';
export interface ServerToClientEvents {
withAck: (d: string, callback: (e: number) => void) => void;
onWhatsApp: onWhatsAppType;
profilePictureUrl: ProfilePictureUrlType;
assertSessions: AssertSessionsType;
createParticipantNodes: CreateParticipantNodesType;
getUSyncDevices: GetUSyncDevicesType;
generateMessageTag: GenerateMessageTagType;
sendNode: SendNodeType;
'signalRepository:decryptMessage': SignalRepositoryDecryptMessageType;
}
export interface ClientToServerEvents {
init: (
me: Contact | undefined,
account: proto.IADVSignedDeviceIdentity | undefined,
status: WAConnectionState,
) => void;
'CB:call': (packet: any) => void;
'CB:ack,class:call': (packet: any) => void;
'connection.update:status': (
me: Contact | undefined,
account: proto.IADVSignedDeviceIdentity | undefined,
status: WAConnectionState,
) => void;
'connection.update:qr': (qr: string) => void;
}
export type onWhatsAppType = (jid: string, callback: onWhatsAppCallback) => void;
export type onWhatsAppCallback = (
response: {
exists: boolean;
jid: string;
}[],
) => void;
export type ProfilePictureUrlType = (
jid: string,
type: 'image' | 'preview',
timeoutMs: number | undefined,
callback: ProfilePictureUrlCallback,
) => void;
export type ProfilePictureUrlCallback = (response: string | undefined) => void;
export type AssertSessionsType = (jids: string[], force: boolean, callback: AssertSessionsCallback) => void;
export type AssertSessionsCallback = (response: boolean) => void;
export type CreateParticipantNodesType = (
jids: string[],
message: any,
extraAttrs: any,
callback: CreateParticipantNodesCallback,
) => void;
export type CreateParticipantNodesCallback = (nodes: any, shouldIncludeDeviceIdentity: boolean) => void;
export type GetUSyncDevicesType = (
jids: string[],
useCache: boolean,
ignoreZeroDevices: boolean,
callback: GetUSyncDevicesTypeCallback,
) => void;
export type GetUSyncDevicesTypeCallback = (jids: JidWithDevice[]) => void;
export type GenerateMessageTagType = (callback: GenerateMessageTagTypeCallback) => void;
export type GenerateMessageTagTypeCallback = (response: string) => void;
export type SendNodeType = (stanza: BinaryNode, callback: SendNodeTypeCallback) => void;
export type SendNodeTypeCallback = (response: boolean) => void;
export type SignalRepositoryDecryptMessageType = (
jid: string,
type: 'pkmsg' | 'msg',
ciphertext: Buffer,
callback: SignalRepositoryDecryptMessageCallback,
) => void;
export type SignalRepositoryDecryptMessageCallback = (response: any) => void;

View File

@ -0,0 +1,181 @@
import { ConnectionState, WAConnectionState, WASocket } from 'baileys';
import { io, Socket } from 'socket.io-client';
import { ClientToServerEvents, ServerToClientEvents } from './transport.type';
let baileys_connection_state: WAConnectionState = 'close';
export const useVoiceCallsBaileys = async (
wavoip_token: string,
baileys_sock: WASocket,
status?: WAConnectionState,
logger?: boolean,
) => {
baileys_connection_state = status ?? 'close';
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('https://devices.wavoip.com/baileys', {
transports: ['websocket'],
path: `/${wavoip_token}/websocket`,
});
socket.on('connect', () => {
if (logger) console.log('[*] - Wavoip connected', socket.id);
socket.emit(
'init',
baileys_sock.authState.creds.me,
baileys_sock.authState.creds.account,
baileys_connection_state,
);
});
socket.on('disconnect', () => {
if (logger) console.log('[*] - Wavoip disconnect');
});
socket.on('connect_error', (error) => {
if (socket.active) {
if (logger)
console.log(
'[*] - Wavoip connection error temporary failure, the socket will automatically try to reconnect',
error,
);
} else {
if (logger) console.log('[*] - Wavoip connection error', error.message);
}
});
socket.on('onWhatsApp', async (jid, callback) => {
try {
const response: any = await baileys_sock.onWhatsApp(jid);
callback(response);
if (logger) console.log('[*] Success on call onWhatsApp function', response, jid);
} catch (error) {
if (logger) console.error('[*] Error on call onWhatsApp function', error);
}
});
socket.on('profilePictureUrl', async (jid, type, timeoutMs, callback) => {
try {
const response = await baileys_sock.profilePictureUrl(jid, type, timeoutMs);
callback(response);
if (logger) console.log('[*] Success on call profilePictureUrl function', response);
} catch (error) {
if (logger) console.error('[*] Error on call profilePictureUrl function', error);
}
});
socket.on('assertSessions', async (jids, force, callback) => {
try {
const response = await baileys_sock.assertSessions(jids, force);
callback(response);
if (logger) console.log('[*] Success on call assertSessions function', response);
} catch (error) {
if (logger) console.error('[*] Error on call assertSessions function', error);
}
});
socket.on('createParticipantNodes', async (jids, message, extraAttrs, callback) => {
try {
const response = await baileys_sock.createParticipantNodes(jids, message, extraAttrs);
callback(response, true);
if (logger) console.log('[*] Success on call createParticipantNodes function', response);
} catch (error) {
if (logger) console.error('[*] Error on call createParticipantNodes function', error);
}
});
socket.on('getUSyncDevices', async (jids, useCache, ignoreZeroDevices, callback) => {
try {
const response = await baileys_sock.getUSyncDevices(jids, useCache, ignoreZeroDevices);
callback(response);
if (logger) console.log('[*] Success on call getUSyncDevices function', response);
} catch (error) {
if (logger) console.error('[*] Error on call getUSyncDevices function', error);
}
});
socket.on('generateMessageTag', async (callback) => {
try {
const response = await baileys_sock.generateMessageTag();
callback(response);
if (logger) console.log('[*] Success on call generateMessageTag function', response);
} catch (error) {
if (logger) console.error('[*] Error on call generateMessageTag function', error);
}
});
socket.on('sendNode', async (stanza, callback) => {
try {
console.log('sendNode', JSON.stringify(stanza));
const response = await baileys_sock.sendNode(stanza);
callback(true);
if (logger) console.log('[*] Success on call sendNode function', response);
} catch (error) {
if (logger) console.error('[*] Error on call sendNode function', error);
}
});
socket.on('signalRepository:decryptMessage', async (jid, type, ciphertext, callback) => {
try {
const response = await baileys_sock.signalRepository.decryptMessage({
jid: jid,
type: type,
ciphertext: ciphertext,
});
callback(response);
if (logger) console.log('[*] Success on call signalRepository:decryptMessage function', response);
} catch (error) {
if (logger) console.error('[*] Error on call signalRepository:decryptMessage function', error);
}
});
// we only use this connection data to inform the webphone that the device is connected and creeds account to generate e2e whatsapp key for make call packets
baileys_sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
const { connection } = update;
if (connection) {
baileys_connection_state = connection;
socket
.timeout(1000)
.emit(
'connection.update:status',
baileys_sock.authState.creds.me,
baileys_sock.authState.creds.account,
connection,
);
}
if (update.qr) {
socket.timeout(1000).emit('connection.update:qr', update.qr);
}
});
baileys_sock.ws.on('CB:call', (packet) => {
if (logger) console.log('[*] Signling received');
socket.volatile.timeout(1000).emit('CB:call', packet);
});
baileys_sock.ws.on('CB:ack,class:call', (packet) => {
if (logger) console.log('[*] Signling ack received');
socket.volatile.timeout(1000).emit('CB:ack,class:call', packet);
});
return socket;
};

View File

@ -131,6 +131,7 @@ import { waMonitor } from '../../server.module';
import { Events, MessageSubtype, TypeMediaMessage, wa } from '../../types/wa.types';
import { CacheService } from './../cache.service';
import { ChannelStartupService } from './../channel.service';
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
@ -669,10 +670,32 @@ export class BaileysStartupService extends ChannelStartupService {
this.logger.verbose('Socket created');
if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
useVoiceCallsBaileys(this.localSettings.wavoipToken, this.client, this.connectionStatus.state as any, true);
}
this.eventHandler();
this.logger.verbose('Socket event handler initialized');
this.client.ws.on('CB:call', (packet) => {
console.log('CB:call', packet);
const payload = {
event: 'CB:call',
packet: packet,
};
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
});
this.client.ws.on('CB:ack,class:call', (packet) => {
console.log('CB:ack,class:call', packet);
const payload = {
event: 'CB:ack,class:call',
packet: packet,
};
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
});
this.phoneNumber = number;
return this.client;
@ -1030,7 +1053,7 @@ export class BaileysStartupService extends ChannelStartupService {
await this.contactHandle['contacts.upsert'](
contacts
.filter((c) => !!c.notify ?? !!c.name)
.filter((c) => !!c.notify || !!c.name)
.map((c) => ({
id: c.id,
name: c.name ?? c.notify,
@ -1117,13 +1140,9 @@ export class BaileysStartupService extends ChannelStartupService {
const contentMsg = received?.message[getContentType(received.message)] as any;
if (
(
this.localWebhook.webhook_base64 === true ||
(
this.configService.get<Websocket>('WEBSOCKET').GLOBAL_EVENTS === true &&
this.configService.get<Websocket>('WEBSOCKET').ENABLED === true
)
) &&
(this.localWebhook.webhook_base64 === true ||
(this.configService.get<Websocket>('WEBSOCKET').GLOBAL_EVENTS === true &&
this.configService.get<Websocket>('WEBSOCKET').ENABLED === true)) &&
isMedia
) {
const buffer = await downloadMediaMessage(
@ -1975,13 +1994,9 @@ export class BaileysStartupService extends ChannelStartupService {
console.log('isMedia', isMedia);
if (
(
this.localWebhook.webhook_base64 === true ||
(
this.configService.get<Websocket>('WEBSOCKET').GLOBAL_EVENTS === true &&
this.configService.get<Websocket>('WEBSOCKET').ENABLED === true
)
) &&
(this.localWebhook.webhook_base64 === true ||
(this.configService.get<Websocket>('WEBSOCKET').GLOBAL_EVENTS === true &&
this.configService.get<Websocket>('WEBSOCKET').ENABLED === true)) &&
isMedia
) {
const buffer = await downloadMediaMessage(

View File

@ -83,6 +83,7 @@ export declare namespace wa {
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
wavoipToken?: string;
};
export type LocalWebsocket = {

View File

@ -1002,9 +1002,26 @@ export const settingsSchema: JSONSchema7 = {
read_messages: { type: 'boolean', enum: [true, false] },
read_status: { type: 'boolean', enum: [true, false] },
sync_full_history: { type: 'boolean', enum: [true, false] },
wavoipToken: { type: 'string' },
},
required: ['reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status', 'sync_full_history'],
...isNotEmpty('reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status', 'sync_full_history'),
required: [
'reject_call',
'groups_ignore',
'always_online',
'read_messages',
'read_status',
'sync_full_history',
'wavoipToken',
],
...isNotEmpty(
'reject_call',
'groups_ignore',
'always_online',
'read_messages',
'read_status',
'sync_full_history',
'wavoipToken',
),
};
export const websocketSchema: JSONSchema7 = {