From 540467293c832ec4e0ce7a1f69eb13b71e4200ac Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 16 Jan 2025 11:58:33 -0300 Subject: [PATCH] Enhance settings and integrate Baileys controller for WhatsApp functionality - Added `wavoipToken` field to `Setting` model in both MySQL and PostgreSQL schemas. - Updated `package.json` and `package-lock.json` to include `mime-types` and `socket.io-client` dependencies. - Introduced `BaileysController` and `BaileysRouter` for handling WhatsApp interactions. - Refactored media type handling to use `mime-types` instead of `mime` across various services. - Updated DTOs and validation schemas to accommodate the new `wavoipToken` field. - Implemented voice call functionalities using the Wavoip service in the Baileys integration. - Enhanced event handling in the WebSocket controller to support new features. --- package-lock.json | 95 +++++++++ package.json | 3 + prisma/mysql-schema.prisma | 1 + .../migration.sql | 11 ++ prisma/postgresql-schema.prisma | 1 + src/api/controllers/instance.controller.ts | 1 + src/api/dto/instance.dto.ts | 1 + src/api/dto/settings.dto.ts | 1 + .../integrations/channel/channel.router.ts | 4 +- .../evolution/evolution.channel.service.ts | 14 +- .../channel/meta/whatsapp.business.service.ts | 14 +- .../channel/whatsapp/baileys.controller.ts | 60 ++++++ .../channel/whatsapp/baileys.router.ts | 105 ++++++++++ .../whatsapp/voiceCalls/transport.type.ts | 78 ++++++++ .../voiceCalls/useVoiceCallsBaileys.ts | 181 ++++++++++++++++++ .../whatsapp/whatsapp.baileys.service.ts | 117 ++++++++++- .../chatwoot/services/chatwoot.service.ts | 12 +- .../integrations/event/event.controller.ts | 3 +- src/api/integrations/event/event.manager.ts | 1 + .../event/pusher/pusher.controller.ts | 4 + .../event/rabbitmq/rabbitmq.controller.ts | 5 + .../integrations/event/sqs/sqs.controller.ts | 5 + .../event/webhook/webhook.controller.ts | 7 +- .../event/websocket/websocket.controller.ts | 15 ++ src/api/routes/index.router.ts | 6 +- src/api/server.module.ts | 3 +- src/api/services/channel.service.ts | 13 +- src/api/types/wa.types.ts | 1 + src/validate/instance.schema.ts | 1 + src/validate/settings.schema.ts | 21 +- 30 files changed, 748 insertions(+), 36 deletions(-) create mode 100644 prisma/postgresql-migrations/20250116001412_add_wavoip_token_to_settings_table/migration.sql create mode 100644 src/api/integrations/channel/whatsapp/baileys.controller.ts create mode 100644 src/api/integrations/channel/whatsapp/baileys.router.ts create mode 100644 src/api/integrations/channel/whatsapp/voiceCalls/transport.type.ts create mode 100644 src/api/integrations/channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts diff --git a/package-lock.json b/package-lock.json index dde026f05..9f6f1904c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "long": "^5.2.3", "mediainfo.js": "^0.3.4", "mime": "^4.0.0", + "mime-types": "^2.1.35", "minio": "^8.0.3", "multer": "^1.4.5-lts.1", "node-cache": "^5.1.2", @@ -53,6 +54,7 @@ "redis": "^4.7.0", "sharp": "^0.32.6", "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", "tsup": "^8.3.5" }, "devDependencies": { @@ -61,6 +63,7 @@ "@types/express": "^4.17.18", "@types/json-schema": "^7.0.15", "@types/mime": "^4.0.0", + "@types/mime-types": "^2.1.4", "@types/node": "^22.10.5", "@types/node-cron": "^3.0.11", "@types/qrcode": "^1.5.5", @@ -3819,6 +3822,12 @@ "mime": "*" } }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true + }, "node_modules/@types/mysql": { "version": "2.15.26", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", @@ -6089,6 +6098,54 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", + "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -10728,6 +10785,36 @@ } } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -12102,6 +12189,14 @@ "node": ">=4.0" } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 1ab53c9a0..dd5976e93 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "long": "^5.2.3", "mediainfo.js": "^0.3.4", "mime": "^4.0.0", + "mime-types": "^2.1.35", "minio": "^8.0.3", "multer": "^1.4.5-lts.1", "node-cache": "^5.1.2", @@ -93,6 +94,7 @@ "redis": "^4.7.0", "sharp": "^0.32.6", "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", "tsup": "^8.3.5" }, "devDependencies": { @@ -101,6 +103,7 @@ "@types/express": "^4.17.18", "@types/json-schema": "^7.0.15", "@types/mime": "^4.0.0", + "@types/mime-types": "^2.1.4", "@types/node": "^22.10.5", "@types/node-cron": "^3.0.11", "@types/qrcode": "^1.5.5", diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index 15b3f3aec..a73ca0693 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -264,6 +264,7 @@ model Setting { readMessages Boolean @default(false) readStatus Boolean @default(false) syncFullHistory Boolean @default(false) + wavoipToken String? @db.VarChar(100) createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) diff --git a/prisma/postgresql-migrations/20250116001412_add_wavoip_token_to_settings_table/migration.sql b/prisma/postgresql-migrations/20250116001412_add_wavoip_token_to_settings_table/migration.sql new file mode 100644 index 000000000..e3aba66f9 --- /dev/null +++ b/prisma/postgresql-migrations/20250116001412_add_wavoip_token_to_settings_table/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "Setting" ADD COLUMN "wavoipToken" VARCHAR(100); + +-- CreateIndex +CREATE UNIQUE INDEX "Chat_remoteJid_instanceId_key" ON "Chat"("remoteJid", "instanceId"); diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index eaff59e58..688ee5dd6 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -265,6 +265,7 @@ model Setting { readMessages Boolean @default(false) @db.Boolean readStatus Boolean @default(false) @db.Boolean syncFullHistory Boolean @default(false) @db.Boolean + wavoipToken String? @db.VarChar(100) createdAt DateTime? @default(now()) @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) diff --git a/src/api/controllers/instance.controller.ts b/src/api/controllers/instance.controller.ts index f68379e89..99fc13f2b 100644 --- a/src/api/controllers/instance.controller.ts +++ b/src/api/controllers/instance.controller.ts @@ -119,6 +119,7 @@ export class InstanceController { readMessages: instanceData.readMessages === true, readStatus: instanceData.readStatus === true, syncFullHistory: instanceData.syncFullHistory === true, + wavoipToken: instanceData.wavoipToken || '', }; await this.settingsService.create(instance, settings); diff --git a/src/api/dto/instance.dto.ts b/src/api/dto/instance.dto.ts index 58298d578..a59e11fe1 100644 --- a/src/api/dto/instance.dto.ts +++ b/src/api/dto/instance.dto.ts @@ -19,6 +19,7 @@ export class InstanceDto extends IntegrationDto { readMessages?: boolean; readStatus?: boolean; syncFullHistory?: boolean; + wavoipToken?: string; // proxy proxyHost?: string; proxyPort?: string; diff --git a/src/api/dto/settings.dto.ts b/src/api/dto/settings.dto.ts index 09302a42e..4779ef499 100644 --- a/src/api/dto/settings.dto.ts +++ b/src/api/dto/settings.dto.ts @@ -6,4 +6,5 @@ export class SettingsDto { readMessages?: boolean; readStatus?: boolean; syncFullHistory?: boolean; + wavoipToken?: string; } diff --git a/src/api/integrations/channel/channel.router.ts b/src/api/integrations/channel/channel.router.ts index c5bce859b..5d878471f 100644 --- a/src/api/integrations/channel/channel.router.ts +++ b/src/api/integrations/channel/channel.router.ts @@ -2,14 +2,16 @@ import { Router } from 'express'; import { EvolutionRouter } from './evolution/evolution.router'; import { MetaRouter } from './meta/meta.router'; +import { BaileysRouter } from './whatsapp/baileys.router'; export class ChannelRouter { public readonly router: Router; - constructor(configService: any) { + constructor(configService: any, ...guards: any[]) { this.router = Router(); this.router.use('/', new EvolutionRouter(configService).router); this.router.use('/', new MetaRouter(configService).router); + this.router.use('/baileys', new BaileysRouter(...guards).router); } } diff --git a/src/api/integrations/channel/evolution/evolution.channel.service.ts b/src/api/integrations/channel/evolution/evolution.channel.service.ts index 10b28a4db..580426cbf 100644 --- a/src/api/integrations/channel/evolution/evolution.channel.service.ts +++ b/src/api/integrations/channel/evolution/evolution.channel.service.ts @@ -10,7 +10,7 @@ import { BadRequestException, InternalServerErrorException } from '@exceptions'; import { status } from '@utils/renderStatus'; import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; -import mime from 'mime'; +import mimeTypes from 'mime-types'; import { v4 } from 'uuid'; export class EvolutionStartupService extends ChannelStartupService { @@ -396,7 +396,7 @@ export class EvolutionStartupService extends ChannelStartupService { mediaMessage.fileName = 'video.mp4'; } - let mimetype: string; + let mimetype: string | false; const prepareMedia: any = { caption: mediaMessage?.caption, @@ -407,9 +407,9 @@ export class EvolutionStartupService extends ChannelStartupService { }; if (isURL(mediaMessage.media)) { - mimetype = mime.getType(mediaMessage.media); + mimetype = mimeTypes.lookup(mediaMessage.media); } else { - mimetype = mime.getType(mediaMessage.fileName); + mimetype = mimeTypes.lookup(mediaMessage.fileName); } prepareMedia.mimetype = mimetype; @@ -449,7 +449,7 @@ export class EvolutionStartupService extends ChannelStartupService { number = number.replace(/\D/g, ''); const hash = `${number}-${new Date().getTime()}`; - let mimetype: string; + let mimetype: string | false; const prepareMedia: any = { fileName: `${hash}.mp4`, @@ -458,9 +458,9 @@ export class EvolutionStartupService extends ChannelStartupService { }; if (isURL(audio)) { - mimetype = mime.getType(audio); + mimetype = mimeTypes.lookup(audio); } else { - mimetype = mime.getType(prepareMedia.fileName); + mimetype = mimeTypes.lookup(prepareMedia.fileName); } prepareMedia.mimetype = mimetype; diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 54c0c73a4..b79e02228 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -28,7 +28,7 @@ import { arrayUnique, isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; import FormData from 'form-data'; import { createReadStream } from 'fs'; -import mime from 'mime'; +import mimeTypes from 'mime-types'; import { join } from 'path'; export class BusinessStartupService extends ChannelStartupService { @@ -1017,7 +1017,7 @@ export class BusinessStartupService extends ChannelStartupService { mediaMessage.fileName = 'video.mp4'; } - let mimetype: string; + let mimetype: string | false; const prepareMedia: any = { caption: mediaMessage?.caption, @@ -1028,11 +1028,11 @@ export class BusinessStartupService extends ChannelStartupService { }; if (isURL(mediaMessage.media)) { - mimetype = mime.getType(mediaMessage.media); + mimetype = mimeTypes.lookup(mediaMessage.media); prepareMedia.id = mediaMessage.media; prepareMedia.type = 'link'; } else { - mimetype = mime.getType(mediaMessage.fileName); + mimetype = mimeTypes.lookup(mediaMessage.fileName); const id = await this.getIdMedia(prepareMedia); prepareMedia.id = id; prepareMedia.type = 'id'; @@ -1075,7 +1075,7 @@ export class BusinessStartupService extends ChannelStartupService { number = number.replace(/\D/g, ''); const hash = `${number}-${new Date().getTime()}`; - let mimetype: string; + let mimetype: string | false; const prepareMedia: any = { fileName: `${hash}.mp3`, @@ -1084,11 +1084,11 @@ export class BusinessStartupService extends ChannelStartupService { }; if (isURL(audio)) { - mimetype = mime.getType(audio); + mimetype = mimeTypes.lookup(audio); prepareMedia.id = audio; prepareMedia.type = 'link'; } else { - mimetype = mime.getType(prepareMedia.fileName); + mimetype = mimeTypes.lookup(prepareMedia.fileName); const id = await this.getIdMedia(prepareMedia); prepareMedia.id = id; prepareMedia.type = 'id'; diff --git a/src/api/integrations/channel/whatsapp/baileys.controller.ts b/src/api/integrations/channel/whatsapp/baileys.controller.ts new file mode 100644 index 000000000..ee547338d --- /dev/null +++ b/src/api/integrations/channel/whatsapp/baileys.controller.ts @@ -0,0 +1,60 @@ +import { InstanceDto } from '@api/dto/instance.dto'; +import { WAMonitoringService } from '@api/services/monitor.service'; + +export class BaileysController { + constructor(private readonly waMonitor: WAMonitoringService) {} + + public async onWhatsapp({ instanceName }: InstanceDto, body: any) { + const instance = this.waMonitor.waInstances[instanceName]; + + return instance.baileysOnWhatsapp(body?.jid); + } + + public async profilePictureUrl({ instanceName }: InstanceDto, body: any) { + const instance = this.waMonitor.waInstances[instanceName]; + + return instance.baileysProfilePictureUrl(body?.jid, body?.type, body?.timeoutMs); + } + + public async assertSessions({ instanceName }: InstanceDto, body: any) { + const instance = this.waMonitor.waInstances[instanceName]; + + return instance.baileysAssertSessions(body?.jids, body?.force); + } + + public async createParticipantNodes({ instanceName }: InstanceDto, body: any) { + const instance = this.waMonitor.waInstances[instanceName]; + + return instance.baileysCreateParticipantNodes(body?.jids, body?.message, body?.extraAttrs); + } + + public async getUSyncDevices({ instanceName }: InstanceDto, body: any) { + const instance = this.waMonitor.waInstances[instanceName]; + + return instance.baileysGetUSyncDevices(body?.jids, body?.useCache, body?.ignoreZeroDevices); + } + + public async generateMessageTag({ instanceName }: InstanceDto) { + const instance = this.waMonitor.waInstances[instanceName]; + + return instance.baileysGenerateMessageTag(); + } + + public async sendNode({ instanceName }: InstanceDto, body: any) { + const instance = this.waMonitor.waInstances[instanceName]; + + return instance.baileysSendNode(body?.stanza); + } + + public async signalRepositoryDecryptMessage({ instanceName }: InstanceDto, body: any) { + const instance = this.waMonitor.waInstances[instanceName]; + + return instance.baileysSignalRepositoryDecryptMessage(body?.jid, body?.type, body?.ciphertext); + } + + public async getAuthState({ instanceName }: InstanceDto) { + const instance = this.waMonitor.waInstances[instanceName]; + + return instance.baileysGetAuthState(); + } +} diff --git a/src/api/integrations/channel/whatsapp/baileys.router.ts b/src/api/integrations/channel/whatsapp/baileys.router.ts new file mode 100644 index 000000000..04a1d565f --- /dev/null +++ b/src/api/integrations/channel/whatsapp/baileys.router.ts @@ -0,0 +1,105 @@ +import { RouterBroker } from '@api/abstract/abstract.router'; +import { InstanceDto } from '@api/dto/instance.dto'; +import { HttpStatus } from '@api/routes/index.router'; +import { baileysController } from '@api/server.module'; +import { instanceSchema } from '@validate/instance.schema'; +import { RequestHandler, Router } from 'express'; + +export class BaileysRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('onWhatsapp'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => baileysController.onWhatsapp(instance, req.body), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('profilePictureUrl'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => baileysController.profilePictureUrl(instance, req.body), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('assertSessions'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => baileysController.assertSessions(instance, req.body), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('createParticipantNodes'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => baileysController.createParticipantNodes(instance, req.body), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('getUSyncDevices'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => baileysController.getUSyncDevices(instance, req.body), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('generateMessageTag'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => baileysController.generateMessageTag(instance), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('sendNode'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => baileysController.sendNode(instance, req.body), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('signalRepositoryDecryptMessage'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => baileysController.signalRepositoryDecryptMessage(instance, req.body), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('getAuthState'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => baileysController.getAuthState(instance), + }); + + res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router: Router = Router(); +} diff --git a/src/api/integrations/channel/whatsapp/voiceCalls/transport.type.ts b/src/api/integrations/channel/whatsapp/voiceCalls/transport.type.ts new file mode 100644 index 000000000..f03c10289 --- /dev/null +++ b/src/api/integrations/channel/whatsapp/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/integrations/channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts b/src/api/integrations/channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts new file mode 100644 index 000000000..951be1a05 --- /dev/null +++ b/src/api/integrations/channel/whatsapp/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/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 0134d276d..06a73c1d5 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -131,7 +131,7 @@ import ffmpeg from 'fluent-ffmpeg'; import FormData from 'form-data'; import { readFileSync } from 'fs'; import Long from 'long'; -import mime from 'mime'; +import mimeTypes from 'mime-types'; import NodeCache from 'node-cache'; import cron from 'node-cron'; import { release } from 'os'; @@ -143,6 +143,8 @@ import sharp from 'sharp'; import { PassThrough, Readable } from 'stream'; import { v4 } from 'uuid'; +import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; + const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine()); // Adicione a função getVideoDuration no início do arquivo @@ -673,8 +675,30 @@ export class BaileysStartupService extends ChannelStartupService { this.client = makeWASocket(socketConfig); + if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) { + useVoiceCallsBaileys(this.localSettings.wavoipToken, this.client, this.connectionStatus.state as any, true); + } + this.eventHandler(); + 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; @@ -1248,7 +1272,7 @@ export class BaileysStartupService extends ChannelStartupService { ); const { buffer, mediaType, fileName, size } = media; - const mimetype = mime.getType(fileName).toString(); + const mimetype = mimeTypes.lookup(fileName).toString(); const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName); await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype, @@ -2210,7 +2234,7 @@ export class BaileysStartupService extends ChannelStartupService { const { buffer, mediaType, fileName, size } = media; - const mimetype = mime.getType(fileName).toString(); + const mimetype = mimeTypes.lookup(fileName).toString(); const fullName = join( `${this.instance.id}`, @@ -2532,12 +2556,12 @@ export class BaileysStartupService extends ChannelStartupService { mediaMessage.fileName = 'video.mp4'; } - let mimetype: string; + let mimetype: string | false; if (mediaMessage.mimetype) { mimetype = mediaMessage.mimetype; } else { - mimetype = mime.getType(mediaMessage.fileName); + mimetype = mimeTypes.lookup(mediaMessage.fileName); if (!mimetype && isURL(mediaMessage.media)) { let config: any = { @@ -3590,7 +3614,7 @@ export class BaileysStartupService extends ChannelStartupService { ); const typeMessage = getContentType(msg.message); - const ext = mime.getExtension(mediaMessage?.['mimetype']); + const ext = mimeTypes.extension(mediaMessage?.['mimetype']); const fileName = mediaMessage?.['fileName'] || `${msg.key.id}.${ext}` || `${v4()}.${ext}`; if (convertToMp4 && typeMessage === 'audioMessage') { @@ -4395,4 +4419,85 @@ export class BaileysStartupService extends ChannelStartupService { id, ); } + + public async baileysOnWhatsapp(jid: string) { + const response = await this.client.onWhatsApp(jid); + + return response; + } + + public async baileysProfilePictureUrl(jid: string, type: 'image' | 'preview', timeoutMs: number) { + const response = await this.client.profilePictureUrl(jid, type, timeoutMs); + + return response; + } + + public async baileysAssertSessions(jids: string[], force: boolean) { + const response = await this.client.assertSessions(jids, force); + + return response; + } + + public async baileysCreateParticipantNodes(jids: string[], message: proto.IMessage, extraAttrs: any) { + const response = await this.client.createParticipantNodes(jids, message, extraAttrs); + + const convertedResponse = { + ...response, + nodes: response.nodes.map((node: any) => ({ + ...node, + content: node.content?.map((c: any) => ({ + ...c, + content: c.content instanceof Uint8Array ? Buffer.from(c.content).toString('base64') : c.content, + })), + })), + }; + + return convertedResponse; + } + + public async baileysSendNode(stanza: any) { + console.log('stanza', JSON.stringify(stanza)); + const response = await this.client.sendNode(stanza); + + return response; + } + + public async baileysGetUSyncDevices(jids: string[], useCache: boolean, ignoreZeroDevices: boolean) { + const response = await this.client.getUSyncDevices(jids, useCache, ignoreZeroDevices); + + return response; + } + + public async baileysGenerateMessageTag() { + const response = await this.client.generateMessageTag(); + + return response; + } + + public async baileysSignalRepositoryDecryptMessage(jid: string, type: 'pkmsg' | 'msg', ciphertext: string) { + try { + const ciphertextBuffer = Buffer.from(ciphertext, 'base64'); + + const response = await this.client.signalRepository.decryptMessage({ + jid, + type, + ciphertext: ciphertextBuffer, + }); + + return response instanceof Uint8Array ? Buffer.from(response).toString('base64') : response; + } catch (error) { + this.logger.error('Error decrypting message:'); + this.logger.error(error); + throw error; + } + } + + public async baileysGetAuthState() { + const response = { + me: this.client.authState.creds.me, + account: this.client.authState.creds.account, + }; + + return response; + } } diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 18005df91..77b58bbe7 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -28,7 +28,7 @@ import dayjs from 'dayjs'; import FormData from 'form-data'; import Jimp from 'jimp'; import Long from 'long'; -import mime from 'mime'; +import mimeTypes from 'mime-types'; import path from 'path'; import { Readable } from 'stream'; @@ -1066,7 +1066,7 @@ export class ChatwootService { public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) { try { const parsedMedia = path.parse(decodeURIComponent(media)); - let mimeType = mime.getType(parsedMedia?.ext) || ''; + let mimeType = mimeTypes.lookup(parsedMedia?.ext) || ''; let fileName = parsedMedia?.name + parsedMedia?.ext; if (!mimeType) { @@ -1958,7 +1958,7 @@ export class ChatwootService { } if (!nameFile) { - nameFile = `${Math.random().toString(36).substring(7)}.${mime.getExtension(downloadBase64.mimetype) || ''}`; + nameFile = `${Math.random().toString(36).substring(7)}.${mimeTypes.extension(downloadBase64.mimetype) || ''}`; } const fileData = Buffer.from(downloadBase64.base64, 'base64'); @@ -2057,8 +2057,8 @@ export class ChatwootService { if (isAdsMessage) { const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' }); - const extension = mime.getExtension(imgBuffer.headers['content-type']); - const mimeType = extension && mime.getType(extension); + const extension = mimeTypes.extension(imgBuffer.headers['content-type']); + const mimeType = extension && mimeTypes.lookup(extension); if (!mimeType) { this.logger.warn('mimetype of Ads message not found'); @@ -2066,7 +2066,7 @@ export class ChatwootService { } const random = Math.random().toString(36).substring(7); - const nameFile = `${random}.${mime.getExtension(mimeType)}`; + const nameFile = `${random}.${mimeTypes.extension(mimeType)}`; const fileData = Buffer.from(imgBuffer.data, 'binary'); const img = await Jimp.read(fileData); diff --git a/src/api/integrations/event/event.controller.ts b/src/api/integrations/event/event.controller.ts index 0afbead55..2e6a23302 100644 --- a/src/api/integrations/event/event.controller.ts +++ b/src/api/integrations/event/event.controller.ts @@ -13,6 +13,7 @@ export type EmitData = { sender: string; apiKey?: string; local?: boolean; + integration?: string[]; }; export interface EventControllerInterface { @@ -23,7 +24,7 @@ export interface EventControllerInterface { export class EventController { public prismaRepository: PrismaRepository; - private waMonitor: WAMonitoringService; + protected waMonitor: WAMonitoringService; private integrationStatus: boolean; private integrationName: string; diff --git a/src/api/integrations/event/event.manager.ts b/src/api/integrations/event/event.manager.ts index b1a21db44..9df96f9f9 100644 --- a/src/api/integrations/event/event.manager.ts +++ b/src/api/integrations/event/event.manager.ts @@ -99,6 +99,7 @@ export class EventManager { sender: string; apiKey?: string; local?: boolean; + integration?: string[]; }): Promise { await this.websocket.emit(eventData); await this.rabbitmq.emit(eventData); diff --git a/src/api/integrations/event/pusher/pusher.controller.ts b/src/api/integrations/event/pusher/pusher.controller.ts index 3f3a74eeb..eef244b2a 100644 --- a/src/api/integrations/event/pusher/pusher.controller.ts +++ b/src/api/integrations/event/pusher/pusher.controller.ts @@ -120,7 +120,11 @@ export class PusherController extends EventController implements EventController sender, apiKey, local, + integration, }: EmitData): Promise { + if (integration && !integration.includes('pusher')) { + return; + } if (!this.status) { return; } diff --git a/src/api/integrations/event/rabbitmq/rabbitmq.controller.ts b/src/api/integrations/event/rabbitmq/rabbitmq.controller.ts index c4e0cc36b..d7623441a 100644 --- a/src/api/integrations/event/rabbitmq/rabbitmq.controller.ts +++ b/src/api/integrations/event/rabbitmq/rabbitmq.controller.ts @@ -73,7 +73,12 @@ export class RabbitmqController extends EventController implements EventControll dateTime, sender, apiKey, + integration, }: EmitData): Promise { + if (integration && !integration.includes('rabbitmq')) { + return; + } + if (!this.status) { return; } diff --git a/src/api/integrations/event/sqs/sqs.controller.ts b/src/api/integrations/event/sqs/sqs.controller.ts index 3c94fe810..1d60fb7bf 100644 --- a/src/api/integrations/event/sqs/sqs.controller.ts +++ b/src/api/integrations/event/sqs/sqs.controller.ts @@ -54,7 +54,12 @@ export class SqsController extends EventController implements EventControllerInt dateTime, sender, apiKey, + integration, }: EmitData): Promise { + if (integration && !integration.includes('sqs')) { + return; + } + if (!this.status) { return; } diff --git a/src/api/integrations/event/webhook/webhook.controller.ts b/src/api/integrations/event/webhook/webhook.controller.ts index 93a2244f7..269a47c01 100644 --- a/src/api/integrations/event/webhook/webhook.controller.ts +++ b/src/api/integrations/event/webhook/webhook.controller.ts @@ -64,7 +64,12 @@ export class WebhookController extends EventController implements EventControlle sender, apiKey, local, + integration, }: EmitData): Promise { + if (integration && !integration.includes('webhook')) { + return; + } + const instance = (await this.get(instanceName)) as wa.LocalWebHook; const webhookConfig = configService.get('WEBHOOK'); @@ -85,7 +90,7 @@ export class WebhookController extends EventController implements EventControlle apikey: apiKey, }; - if ((local && !instance) || !instance?.enabled) { + if (local && instance?.enabled) { if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { let baseURL: string; diff --git a/src/api/integrations/event/websocket/websocket.controller.ts b/src/api/integrations/event/websocket/websocket.controller.ts index e7dab1df3..f6d152ffb 100644 --- a/src/api/integrations/event/websocket/websocket.controller.ts +++ b/src/api/integrations/event/websocket/websocket.controller.ts @@ -35,6 +35,16 @@ export class WebsocketController extends EventController implements EventControl socket.on('disconnect', () => { this.logger.info('User disconnected'); }); + + socket.on('sendNode', async (data) => { + try { + await this.waMonitor.waInstances[data.instanceId].baileysSendNode(data.stanza); + this.logger.info('Node sent successfully'); + } catch (error) { + this.logger.error('Error sending node:'); + this.logger.error(error); + } + }); }); this.logger.info('Socket.io initialized'); @@ -65,7 +75,12 @@ export class WebsocketController extends EventController implements EventControl dateTime, sender, apiKey, + integration, }: EmitData): Promise { + if (integration && !integration.includes('websocket')) { + return; + } + if (!this.status) { return; } diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index 5587f7b6e..a4a7c0717 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -8,7 +8,7 @@ import { StorageRouter } from '@api/integrations/storage/storage.router'; import { configService } from '@config/env.config'; import { Router } from 'express'; import fs from 'fs'; -import mime from 'mime'; +import mimeTypes from 'mime-types'; import path from 'path'; import { CallRouter } from './call.router'; @@ -49,7 +49,7 @@ router.get('/assets/*', (req, res) => { const filePath = path.join(basePath, 'assets/', fileName); if (fs.existsSync(filePath)) { - res.set('Content-Type', mime.getType(filePath) || 'text/css'); + res.set('Content-Type', mimeTypes.lookup(filePath) || 'text/css'); res.send(fs.readFileSync(filePath)); } else { res.status(404).send('File not found'); @@ -87,7 +87,7 @@ router .use('/settings', new SettingsRouter(...guards).router) .use('/proxy', new ProxyRouter(...guards).router) .use('/label', new LabelRouter(...guards).router) - .use('', new ChannelRouter(configService).router) + .use('', new ChannelRouter(configService, ...guards).router) .use('', new EventRouter(configService, ...guards).router) .use('', new ChatbotRouter(...guards).router) .use('', new StorageRouter(...guards).router); diff --git a/src/api/server.module.ts b/src/api/server.module.ts index 84999cb8a..49fc56952 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -15,6 +15,7 @@ import { TemplateController } from './controllers/template.controller'; import { ChannelController } from './integrations/channel/channel.controller'; import { EvolutionController } from './integrations/channel/evolution/evolution.controller'; import { MetaController } from './integrations/channel/meta/meta.controller'; +import { BaileysController } from './integrations/channel/whatsapp/baileys.controller'; import { ChatbotController } from './integrations/chatbot/chatbot.controller'; import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/chatwoot.controller'; import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service'; @@ -107,7 +108,7 @@ export const channelController = new ChannelController(prismaRepository, waMonit // channels export const evolutionController = new EvolutionController(prismaRepository, waMonitor); export const metaController = new MetaController(prismaRepository, waMonitor); - +export const baileysController = new BaileysController(waMonitor); // chatbots const typebotService = new TypebotService(waMonitor, configService, prismaRepository); export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor); diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 84d406769..6b71b4dc3 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -151,6 +151,7 @@ export class ChannelStartupService { this.localSettings.readMessages = data?.readMessages; this.localSettings.readStatus = data?.readStatus; this.localSettings.syncFullHistory = data?.syncFullHistory; + this.localSettings.wavoipToken = data?.wavoipToken; } public async setSettings(data: SettingsDto) { @@ -166,6 +167,7 @@ export class ChannelStartupService { readMessages: data.readMessages, readStatus: data.readStatus, syncFullHistory: data.syncFullHistory, + wavoipToken: data.wavoipToken, }, create: { rejectCall: data.rejectCall, @@ -175,6 +177,7 @@ export class ChannelStartupService { readMessages: data.readMessages, readStatus: data.readStatus, syncFullHistory: data.syncFullHistory, + wavoipToken: data.wavoipToken, instanceId: this.instanceId, }, }); @@ -186,6 +189,12 @@ export class ChannelStartupService { this.localSettings.readMessages = data?.readMessages; this.localSettings.readStatus = data?.readStatus; this.localSettings.syncFullHistory = data?.syncFullHistory; + this.localSettings.wavoipToken = data?.wavoipToken; + + if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) { + this.client.ws.close(); + this.client.ws.connect(); + } } public async findSettings() { @@ -207,6 +216,7 @@ export class ChannelStartupService { readMessages: data.readMessages, readStatus: data.readStatus, syncFullHistory: data.syncFullHistory, + wavoipToken: data.wavoipToken, }; } @@ -419,7 +429,7 @@ export class ChannelStartupService { return data; } - public async sendDataWebhook(event: Events, data: T, local = true) { + public async sendDataWebhook(event: Events, data: T, local = true, integration?: string[]) { const serverUrl = this.configService.get('SERVER').URL; const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds const localISOTime = new Date(Date.now() - tzoffset).toISOString(); @@ -439,6 +449,7 @@ export class ChannelStartupService { sender: this.wuid, apiKey: expose && instanceApikey ? instanceApikey : null, local, + integration, }); } diff --git a/src/api/types/wa.types.ts b/src/api/types/wa.types.ts index 472b37af2..0aad06962 100644 --- a/src/api/types/wa.types.ts +++ b/src/api/types/wa.types.ts @@ -85,6 +85,7 @@ export declare namespace wa { readMessages?: boolean; readStatus?: boolean; syncFullHistory?: boolean; + wavoipToken?: string; }; export type LocalEvent = { diff --git a/src/validate/instance.schema.ts b/src/validate/instance.schema.ts index de5be2b9c..06c34df99 100644 --- a/src/validate/instance.schema.ts +++ b/src/validate/instance.schema.ts @@ -43,6 +43,7 @@ export const instanceSchema: JSONSchema7 = { readMessages: { type: 'boolean' }, readStatus: { type: 'boolean' }, syncFullHistory: { type: 'boolean' }, + wavoipToken: { type: 'string' }, // Proxy proxyHost: { type: 'string' }, proxyPort: { type: 'string' }, diff --git a/src/validate/settings.schema.ts b/src/validate/settings.schema.ts index b5c056010..50cbfe6ae 100644 --- a/src/validate/settings.schema.ts +++ b/src/validate/settings.schema.ts @@ -31,7 +31,24 @@ export const settingsSchema: JSONSchema7 = { readMessages: { type: 'boolean' }, readStatus: { type: 'boolean' }, syncFullHistory: { type: 'boolean' }, + wavoipToken: { type: 'string' }, }, - required: ['rejectCall', 'groupsIgnore', 'alwaysOnline', 'readMessages', 'readStatus', 'syncFullHistory'], - ...isNotEmpty('rejectCall', 'groupsIgnore', 'alwaysOnline', 'readMessages', 'readStatus', 'syncFullHistory'), + required: [ + 'rejectCall', + 'groupsIgnore', + 'alwaysOnline', + 'readMessages', + 'readStatus', + 'syncFullHistory', + 'wavoipToken', + ], + ...isNotEmpty( + 'rejectCall', + 'groupsIgnore', + 'alwaysOnline', + 'readMessages', + 'readStatus', + 'syncFullHistory', + 'wavoipToken', + ), };