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.
This commit is contained in:
Davidson Gomes 2025-01-16 11:58:33 -03:00
parent 616ae0a7eb
commit 540467293c
30 changed files with 748 additions and 36 deletions

95
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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)

View File

@ -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");

View File

@ -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)

View File

@ -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);

View File

@ -19,6 +19,7 @@ export class InstanceDto extends IntegrationDto {
readMessages?: boolean;
readStatus?: boolean;
syncFullHistory?: boolean;
wavoipToken?: string;
// proxy
proxyHost?: string;
proxyPort?: string;

View File

@ -6,4 +6,5 @@ export class SettingsDto {
readMessages?: boolean;
readStatus?: boolean;
syncFullHistory?: boolean;
wavoipToken?: string;
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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';

View File

@ -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();
}
}

View File

@ -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<InstanceDto>({
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<InstanceDto>({
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<InstanceDto>({
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<InstanceDto>({
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<InstanceDto>({
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<InstanceDto>({
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<InstanceDto>({
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<InstanceDto>({
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<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.getAuthState(instance),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

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,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;
}
}

View File

@ -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);

View File

@ -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;

View File

@ -99,6 +99,7 @@ export class EventManager {
sender: string;
apiKey?: string;
local?: boolean;
integration?: string[];
}): Promise<void> {
await this.websocket.emit(eventData);
await this.rabbitmq.emit(eventData);

View File

@ -120,7 +120,11 @@ export class PusherController extends EventController implements EventController
sender,
apiKey,
local,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('pusher')) {
return;
}
if (!this.status) {
return;
}

View File

@ -73,7 +73,12 @@ export class RabbitmqController extends EventController implements EventControll
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('rabbitmq')) {
return;
}
if (!this.status) {
return;
}

View File

@ -54,7 +54,12 @@ export class SqsController extends EventController implements EventControllerInt
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('sqs')) {
return;
}
if (!this.status) {
return;
}

View File

@ -64,7 +64,12 @@ export class WebhookController extends EventController implements EventControlle
sender,
apiKey,
local,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('webhook')) {
return;
}
const instance = (await this.get(instanceName)) as wa.LocalWebHook;
const webhookConfig = configService.get<Webhook>('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;

View File

@ -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<void> {
if (integration && !integration.includes('websocket')) {
return;
}
if (!this.status) {
return;
}

View File

@ -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);

View File

@ -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);

View File

@ -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<T = any>(event: Events, data: T, local = true) {
public async sendDataWebhook<T = any>(event: Events, data: T, local = true, integration?: string[]) {
const serverUrl = this.configService.get<HttpServer>('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,
});
}

View File

@ -85,6 +85,7 @@ export declare namespace wa {
readMessages?: boolean;
readStatus?: boolean;
syncFullHistory?: boolean;
wavoipToken?: string;
};
export type LocalEvent = {

View File

@ -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' },

View File

@ -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',
),
};