From 5043ce84058f394a38448559d744dc6061b35bcc Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 29 May 2025 19:20:09 -0300 Subject: [PATCH] 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. --- CHANGELOG.md | 8 +- package.json | 1 + src/api/controllers/instance.controller.ts | 2 + src/api/dto/instance.dto.ts | 1 + src/api/dto/settings.dto.ts | 1 + src/api/models/settings.model.ts | 2 + src/api/services/channel.service.ts | 23 ++- .../channels/voiceCalls/transport.type.ts | 78 ++++++++ .../voiceCalls/useVoiceCallsBaileys.ts | 181 ++++++++++++++++++ .../channels/whatsapp.baileys.service.ts | 45 +++-- src/api/types/wa.types.ts | 1 + src/validate/validate.schema.ts | 21 +- 12 files changed, 340 insertions(+), 24 deletions(-) create mode 100644 src/api/services/channels/voiceCalls/transport.type.ts create mode 100644 src/api/services/channels/voiceCalls/useVoiceCallsBaileys.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ca39beb2..f6d240f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -# 1.8.6 (develop) +# 1.8.7 + +### Features + +* Wavoip integration + +# 1.8.6 ### Features diff --git a/package.json b/package.json index 895a8fab..d8ecfd89 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/controllers/instance.controller.ts b/src/api/controllers/instance.controller.ts index eb187193..d09461c1 100644 --- a/src/api/controllers/instance.controller.ts +++ b/src/api/controllers/instance.controller.ts @@ -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)); diff --git a/src/api/dto/instance.dto.ts b/src/api/dto/instance.dto.ts index 1f2ff1c6..2dcc703d 100644 --- a/src/api/dto/instance.dto.ts +++ b/src/api/dto/instance.dto.ts @@ -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; diff --git a/src/api/dto/settings.dto.ts b/src/api/dto/settings.dto.ts index 8cd67948..4b35209a 100644 --- a/src/api/dto/settings.dto.ts +++ b/src/api/dto/settings.dto.ts @@ -6,4 +6,5 @@ export class SettingsDto { read_messages?: boolean; read_status?: boolean; sync_full_history?: boolean; + wavoipToken?: string; } diff --git a/src/api/models/settings.model.ts b/src/api/models/settings.model.ts index 64c032ed..756adaad 100644 --- a/src/api/models/settings.model.ts +++ b/src/api/models/settings.model.ts @@ -11,6 +11,7 @@ export class SettingsRaw { read_messages?: boolean; read_status?: boolean; sync_full_history?: boolean; + wavoipToken?: string; } const settingsSchema = new Schema({ @@ -22,6 +23,7 @@ const settingsSchema = new Schema({ 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'); diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 8b9df497..ef3ad24d 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -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(event: Events, data: T, local = true) { + public async sendDataWebhook( + event: Events, + data: T, + local = true, + integration = ['websocket', 'rabbitmq', 'sqs', 'webhook'], + ) { const webhookGlobal = this.configService.get('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')?.ENABLED) { + if (this.configService.get('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('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').GLOBAL; diff --git a/src/api/services/channels/voiceCalls/transport.type.ts b/src/api/services/channels/voiceCalls/transport.type.ts new file mode 100644 index 00000000..f03c1028 --- /dev/null +++ b/src/api/services/channels/voiceCalls/transport.type.ts @@ -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; diff --git a/src/api/services/channels/voiceCalls/useVoiceCallsBaileys.ts b/src/api/services/channels/voiceCalls/useVoiceCallsBaileys.ts new file mode 100644 index 00000000..951be1a0 --- /dev/null +++ b/src/api/services/channels/voiceCalls/useVoiceCallsBaileys.ts @@ -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 = 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) => { + 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; +}; diff --git a/src/api/services/channels/whatsapp.baileys.service.ts b/src/api/services/channels/whatsapp.baileys.service.ts index e7c438e4..a09c0597 100644 --- a/src/api/services/channels/whatsapp.baileys.service.ts +++ b/src/api/services/channels/whatsapp.baileys.service.ts @@ -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').GLOBAL_EVENTS === true && - this.configService.get('WEBSOCKET').ENABLED === true - ) - ) && + (this.localWebhook.webhook_base64 === true || + (this.configService.get('WEBSOCKET').GLOBAL_EVENTS === true && + this.configService.get('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').GLOBAL_EVENTS === true && - this.configService.get('WEBSOCKET').ENABLED === true - ) - ) && + (this.localWebhook.webhook_base64 === true || + (this.configService.get('WEBSOCKET').GLOBAL_EVENTS === true && + this.configService.get('WEBSOCKET').ENABLED === true)) && isMedia ) { const buffer = await downloadMediaMessage( diff --git a/src/api/types/wa.types.ts b/src/api/types/wa.types.ts index 94461964..e380d640 100644 --- a/src/api/types/wa.types.ts +++ b/src/api/types/wa.types.ts @@ -83,6 +83,7 @@ export declare namespace wa { read_messages?: boolean; read_status?: boolean; sync_full_history?: boolean; + wavoipToken?: string; }; export type LocalWebsocket = { diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 8f7cb1a0..e356342c 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -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 = {