diff --git a/.github/workflows/publish_docker_image_latest.yml b/.github/workflows/publish_docker_image_latest.yml new file mode 100644 index 000000000..641dc5e01 --- /dev/null +++ b/.github/workflows/publish_docker_image_latest.yml @@ -0,0 +1,48 @@ +name: Build Docker image + +on: + push: + branches: + - main + +jobs: + build_deploy: + name: Build and Deploy + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: atendai/evolution-api + tags: latest + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v5 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/publish_docker_image_v2.yml b/.github/workflows/publish_docker_image_v2.yml new file mode 100644 index 000000000..8a941b3fd --- /dev/null +++ b/.github/workflows/publish_docker_image_v2.yml @@ -0,0 +1,48 @@ +name: Build Docker image + +on: + push: + branches: + - v2.0.0 + +jobs: + build_deploy: + name: Build and Deploy + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: atendai/evolution-api + tags: v2.0.0-alpha + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v5 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e5adcb7bf..f6d240f88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,53 @@ +# 1.8.7 + +### Features + +* Wavoip integration + +# 1.8.6 + +### Features + +* Adds method to fetch contacts with last message + +### Fixed + +* Retry and Reconnect system in rabbitmq integration + +### Feature + +* RabbitMQ optimization with parameterized settings via environment variables (MESSAGE_TTL, MAX_LENGTH and MAX_LENGTH_BYTES) +* Non-persistent messages to reduce disk usage +* Automatic cleanup of expired messages in queues + +# 1.8.5 (2025-02-03 12:32) + +### Fixed + +* Update Baileys Version + +# 1.8.4 (2025-01-31 10:00) + +### Features + +* Added prefix key to queue name in RabbitMQ + +# 1.8.3 (2024-11-29 10:00) + +### Fixed + +* Fixed issue sending group messages when ignore groups enabled +* Fixed groups_ignore in /instance/create and maintaining compatibility + +# 1.8.2 (2024-07-03 13:50) + +### Fixed + +* Corretion in globall rabbitmq queue name +* Improvement in the use of mongodb database for credentials +* Fixed base64 in webhook for documentWithCaption +* Fixed Generate pairing code + # 1.8.1 (2024-06-08 21:32) ### Feature diff --git a/package.json b/package.json index bcd187d5e..d8ecfd893 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.8.1", + "version": "1.8.6", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { @@ -42,14 +42,14 @@ "homepage": "https://github.com/EvolutionAPI/evolution-api#readme", "dependencies": { "@adiwajshing/keyed-db": "^0.2.4", + "@aws-sdk/client-sqs": "^3.569.0", "@ffmpeg-installer/ffmpeg": "^1.1.0", "@figuro/chatwoot-sdk": "^1.1.16", "@hapi/boom": "^10.0.1", "@sentry/node": "^7.59.2", "amqplib": "^0.10.3", - "@aws-sdk/client-sqs": "^3.569.0", "axios": "^1.6.5", - "@whiskeysockets/baileys": "6.7.4", + "baileys": "github:EvolutionAPI/Baileys", "class-validator": "^0.14.1", "compression": "^1.7.4", "cors": "^2.8.5", @@ -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/chat.controller.ts b/src/api/controllers/chat.controller.ts index 9d22a8f0e..24c048e2f 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -86,6 +86,11 @@ export class ChatController { return await this.waMonitor.waInstances[instanceName].fetchChats(); } + public async fetchContactsWithLastMessage({ instanceName }: InstanceDto) { + logger.verbose('requested fetchContactsWithLastMessage from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchContactsWithLastMessage(); + } + public async sendPresence({ instanceName }: InstanceDto, data: SendPresenceDto) { logger.verbose('requested sendPresence from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].sendPresence(data); diff --git a/src/api/controllers/instance.controller.ts b/src/api/controllers/instance.controller.ts index bc3ace612..d09461c10 100644 --- a/src/api/controllers/instance.controller.ts +++ b/src/api/controllers/instance.controller.ts @@ -1,4 +1,4 @@ -import { delay } from '@whiskeysockets/baileys'; +import { delay } from 'baileys'; import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; import { v4 } from 'uuid'; @@ -78,6 +78,7 @@ export class InstanceController { read_messages, read_status, sync_full_history, + wavoipToken, websocket_enabled, websocket_events, rabbitmq_enabled, @@ -396,11 +397,12 @@ export class InstanceController { const settings: wa.LocalSettings = { reject_call: reject_call || false, msg_call: msg_call || '', - groups_ignore: groups_ignore || true, + groups_ignore: groups_ignore === undefined ? true : groups_ignore || false, always_online: always_online || false, 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)); @@ -737,16 +739,11 @@ export class InstanceController { this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); const { instance } = await this.connectionState({ instanceName }); - if (instance.state === 'open') { - throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected'); - } try { this.waMonitor.waInstances[instanceName]?.removeRabbitmqQueues(); this.waMonitor.waInstances[instanceName]?.clearCacheChatwoot(); - if (instance.state === 'connecting') { - this.logger.verbose('logging out instance: ' + instanceName); - + if (instance.state === 'connecting' || instance.state === 'open') { await this.logout({ instanceName }); } diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index 3ed55d522..e261cc1be 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -1,4 +1,4 @@ -import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys'; +import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from 'baileys'; export class OnWhatsAppDto { constructor( @@ -84,7 +84,7 @@ class PrivacySetting { status: WAPrivacyValue; online: WAPrivacyOnlineValue; last: WAPrivacyValue; - groupadd: WAPrivacyValue; + groupadd: any; } export class PrivacySettingDto { diff --git a/src/api/dto/instance.dto.ts b/src/api/dto/instance.dto.ts index d9a4d87aa..2dcc703d0 100644 --- a/src/api/dto/instance.dto.ts +++ b/src/api/dto/instance.dto.ts @@ -1,4 +1,4 @@ -import { WAPresence } from '@whiskeysockets/baileys'; +import { WAPresence } from 'baileys'; import { ProxyDto } from './proxy.dto'; @@ -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/sendMessage.dto.ts b/src/api/dto/sendMessage.dto.ts index 7bb33074b..5c197d446 100644 --- a/src/api/dto/sendMessage.dto.ts +++ b/src/api/dto/sendMessage.dto.ts @@ -1,4 +1,4 @@ -import { proto, WAPresence } from '@whiskeysockets/baileys'; +import { proto, WAPresence } from 'baileys'; export class Quoted { key: proto.IMessageKey; diff --git a/src/api/dto/settings.dto.ts b/src/api/dto/settings.dto.ts index 8cd679481..4b35209ad 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/integrations/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatwoot/services/chatwoot.service.ts index 9a30dc125..f8ad2b280 100644 --- a/src/api/integrations/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatwoot/services/chatwoot.service.ts @@ -8,8 +8,8 @@ import ChatwootClient, { inbox, } from '@figuro/chatwoot-sdk'; import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request'; -import { proto } from '@whiskeysockets/baileys'; import axios from 'axios'; +import { proto } from 'baileys'; import FormData from 'form-data'; import { createReadStream, unlinkSync, writeFileSync } from 'fs'; import Jimp from 'jimp'; @@ -444,8 +444,7 @@ export class ChatwootService { const searchableFields = this.getSearchableFields(); // eslint-disable-next-line prettier/prettier - if(contacts.length === 2 && this.getClientCwConfig().merge_brazil_contacts && query.startsWith('+55')){ - + if (contacts.length === 2 && this.getClientCwConfig().merge_brazil_contacts && query.startsWith('+55')) { const contact = this.mergeBrazilianContacts(contacts); if (contact) { return contact; @@ -736,7 +735,11 @@ export class ChatwootService { } this.logger.verbose('find inbox by name'); - const findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().name_inbox); + let findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().name_inbox); + + if (!findByName) { + findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().name_inbox.split('-cwId-')[0]); + } if (!findByName) { this.logger.warn('inbox not found'); @@ -1903,7 +1906,8 @@ export class ChatwootService { let nameFile: string; const messageBody = body?.message[body?.messageType]; - const originalFilename = messageBody?.fileName || messageBody?.message?.documentMessage?.fileName; + const originalFilename = + messageBody?.fileName || messageBody?.filename || messageBody?.message?.documentMessage?.fileName; if (originalFilename) { const parsedFile = path.parse(originalFilename); if (parsedFile.name && parsedFile.ext) { diff --git a/src/api/integrations/chatwoot/utils/chatwoot-import-helper.ts b/src/api/integrations/chatwoot/utils/chatwoot-import-helper.ts index dd0bb23a4..e102aa57f 100644 --- a/src/api/integrations/chatwoot/utils/chatwoot-import-helper.ts +++ b/src/api/integrations/chatwoot/utils/chatwoot-import-helper.ts @@ -1,5 +1,5 @@ import { inbox } from '@figuro/chatwoot-sdk'; -import { proto } from '@whiskeysockets/baileys'; +import { proto } from 'baileys'; import { InstanceDto } from '../../../../api/dto/instance.dto'; import { ChatwootRaw, ContactRaw, MessageRaw } from '../../../../api/models'; diff --git a/src/api/integrations/rabbitmq/libs/amqp.server.ts b/src/api/integrations/rabbitmq/libs/amqp.server.ts index 99c10f661..7dfc41136 100644 --- a/src/api/integrations/rabbitmq/libs/amqp.server.ts +++ b/src/api/integrations/rabbitmq/libs/amqp.server.ts @@ -6,45 +6,133 @@ import { Logger } from '../../../../config/logger.config'; const logger = new Logger('AMQP'); let amqpChannel: amqp.Channel | null = null; +let amqpConnection: amqp.Connection | null = null; +let reconnectAttempts = 0; +const maxReconnectAttempts = 10; +const reconnectInterval = 5000; // 5 segundos + +type ResolveCallback = () => void; +type RejectCallback = (error: Error) => void; export const initAMQP = () => { return new Promise((resolve, reject) => { - const uri = configService.get('RABBITMQ').URI; - amqp.connect(uri, (error, connection) => { - if (error) { - reject(error); - return; - } + connectToRabbitMQ(resolve, reject); + }); +}; + +const connectToRabbitMQ = (resolve?: ResolveCallback, reject?: RejectCallback) => { + const uri = configService.get('RABBITMQ').URI; + amqp.connect(uri, (error, connection) => { + if (error) { + logger.error(`Failed to connect to RabbitMQ: ${error.message}`); + handleConnectionError(error, resolve, reject); + return; + } + + reconnectAttempts = 0; + amqpConnection = connection; + + connection.on('error', (err) => { + logger.error(`RabbitMQ connection error: ${err.message}`); + scheduleReconnect(); + }); + + connection.on('close', () => { + logger.warn('RabbitMQ connection closed unexpectedly'); + scheduleReconnect(); + }); + + createChannel(connection, resolve, reject); + }); +}; - connection.createChannel((channelError, channel) => { - if (channelError) { - reject(channelError); - return; - } +const createChannel = (connection: amqp.Connection, resolve?: ResolveCallback, reject?: RejectCallback) => { + connection.createChannel((channelError, channel) => { + if (channelError) { + logger.error(`Failed to create channel: ${channelError.message}`); + if (reject) { + reject(channelError); + } + return; + } - const exchangeName = 'evolution_exchange'; + const exchangeName = 'evolution_exchange'; - channel.assertExchange(exchangeName, 'topic', { - durable: true, - autoDelete: false, - }); + channel.assertExchange(exchangeName, 'topic', { + durable: true, + autoDelete: false, + }); - amqpChannel = channel; + channel.on('error', (err) => { + logger.error(`RabbitMQ channel error: ${err.message}`); + amqpChannel = null; + createChannel(connection); + }); - logger.info('AMQP initialized'); - resolve(); - }); + channel.on('close', () => { + logger.warn('RabbitMQ channel closed'); + amqpChannel = null; + createChannel(connection); }); + + amqpChannel = channel; + + logger.info('AMQP initialized'); + if (resolve) { + resolve(); + } }); }; +const scheduleReconnect = () => { + if (reconnectAttempts >= maxReconnectAttempts) { + logger.error(`Exceeded maximum ${maxReconnectAttempts} reconnection attempts to RabbitMQ`); + return; + } + + amqpChannel = null; + + if (amqpConnection) { + try { + amqpConnection.close(); + } catch (err) { + // Ignora erro ao fechar conexão que já pode estar fechada + } + amqpConnection = null; + } + + reconnectAttempts++; + const delay = reconnectInterval * Math.pow(1.5, reconnectAttempts - 1); // Backoff exponencial + + logger.info(`Reconnection attempt ${reconnectAttempts} to RabbitMQ in ${delay}ms`); + + setTimeout(() => { + connectToRabbitMQ(); + }, delay); +}; + +const handleConnectionError = (error: Error, resolve?: ResolveCallback, reject?: RejectCallback) => { + if (reject && reconnectAttempts === 0) { + // Na inicialização, rejeitar a Promise se for a primeira tentativa + reject(error); + return; + } + + scheduleReconnect(); +}; + export const getAMQP = (): amqp.Channel | null => { return amqpChannel; }; export const initGlobalQueues = () => { logger.info('Initializing global queues'); - const events = configService.get('RABBITMQ').EVENTS; + const rabbitmqConfig = configService.get('RABBITMQ'); + const events = rabbitmqConfig.EVENTS; + const prefixKey = rabbitmqConfig.PREFIX_KEY; + const messageTtl = rabbitmqConfig.MESSAGE_TTL; + const maxLength = rabbitmqConfig.MAX_LENGTH; + const maxLengthBytes = rabbitmqConfig.MAX_LENGTH_BYTES; if (!events) { logger.warn('No events to initialize on AMQP'); @@ -54,9 +142,15 @@ export const initGlobalQueues = () => { const eventKeys = Object.keys(events); eventKeys.forEach((event) => { - if (events[event] === false) return; + if (events[event] === false) { + return; + } + + const queueName = + prefixKey !== '' + ? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}` + : `${event.replace(/_/g, '.').toLowerCase()}`; - const queueName = `${event.replace(/_/g, '.').toLowerCase()}`; const amqp = getAMQP(); const exchangeName = 'evolution_exchange'; @@ -70,6 +164,10 @@ export const initGlobalQueues = () => { autoDelete: false, arguments: { 'x-queue-type': 'quorum', + 'x-message-ttl': messageTtl, + 'x-max-length': maxLength, + 'x-max-length-bytes': maxLengthBytes, + 'x-overflow': 'reject-publish', }, }); @@ -78,7 +176,14 @@ export const initGlobalQueues = () => { }; export const initQueues = (instanceName: string, events: string[]) => { - if (!events || !events.length) return; + if (!events || !events.length) { + return; + } + + const rabbitmqConfig = configService.get('RABBITMQ'); + const messageTtl = rabbitmqConfig.MESSAGE_TTL; + const maxLength = rabbitmqConfig.MAX_LENGTH; + const maxLengthBytes = rabbitmqConfig.MAX_LENGTH_BYTES; const queues = events.map((event) => { return `${event.replace(/_/g, '.').toLowerCase()}`; @@ -100,6 +205,10 @@ export const initQueues = (instanceName: string, events: string[]) => { autoDelete: false, arguments: { 'x-queue-type': 'quorum', + 'x-message-ttl': messageTtl, + 'x-max-length': maxLength, + 'x-max-length-bytes': maxLengthBytes, + 'x-overflow': 'reject-publish', }, }); @@ -108,7 +217,9 @@ export const initQueues = (instanceName: string, events: string[]) => { }; export const removeQueues = (instanceName: string, events: string[]) => { - if (!events || !events.length) return; + if (!events || !events.length) { + return; + } const channel = getAMQP(); diff --git a/src/api/integrations/websocket/libs/socket.server.ts b/src/api/integrations/websocket/libs/socket.server.ts index 81f97847c..d03c6283b 100644 --- a/src/api/integrations/websocket/libs/socket.server.ts +++ b/src/api/integrations/websocket/libs/socket.server.ts @@ -8,13 +8,17 @@ const logger = new Logger('Socket'); let io: SocketIO; -const cors = configService.get('CORS').ORIGIN; +const origin = configService.get('CORS').ORIGIN; +const methods = configService.get('CORS').METHODS; +const credentials = configService.get('CORS').CREDENTIALS; export const initIO = (httpServer: Server) => { if (configService.get('WEBSOCKET')?.ENABLED) { io = new SocketIO(httpServer, { cors: { - origin: cors, + origin, + methods, + credentials, }, }); diff --git a/src/api/models/message.model.ts b/src/api/models/message.model.ts index 95cb55133..39ba60c4a 100644 --- a/src/api/models/message.model.ts +++ b/src/api/models/message.model.ts @@ -1,3 +1,4 @@ +import Long from 'long'; import { Schema } from 'mongoose'; import { dbserver } from '../../libs/db.connect'; @@ -23,9 +24,9 @@ export class MessageRaw { key?: Key; pushName?: string; participant?: string; - message?: object; + message?: any; messageType?: string; - messageTimestamp?: number | Long.Long; + messageTimestamp?: number | Long; owner: string; source?: 'android' | 'web' | 'ios' | 'unknown' | 'desktop'; source_id?: string; diff --git a/src/api/models/settings.model.ts b/src/api/models/settings.model.ts index 64c032ed9..756adaadf 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/routes/chat.router.ts b/src/api/routes/chat.router.ts index 4debf3d13..f4b6426fb 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -253,6 +253,23 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) + .get(this.routerPath('fetchContactsWithLastMessage'), ...guards, async (req, res) => { + logger.verbose('request received in fetchContactsWithLastMessage'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: InstanceDto, + execute: (instance) => chatController.fetchContactsWithLastMessage(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) .post(this.routerPath('sendPresence'), ...guards, async (req, res) => { logger.verbose('request received in sendPresence'); logger.verbose('request body: '); diff --git a/src/api/routes/instance.router.ts b/src/api/routes/instance.router.ts index d2cc6fb88..d16f97677 100644 --- a/src/api/routes/instance.router.ts +++ b/src/api/routes/instance.router.ts @@ -99,22 +99,28 @@ export class InstanceRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .get(this.routerPath('fetchInstances', false), ...guards, async (req, res) => { - logger.verbose('request received in fetchInstances'); - logger.verbose('request body: '); - logger.verbose(req.body); - - const key = req.get('apikey'); - - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: null, - ClassRef: InstanceDto, - execute: (instance) => instanceController.fetchInstances(instance, key), - }); - - return res.status(HttpStatus.OK).json(response); + try { + logger.verbose('request received in fetchInstances'); + logger.verbose('request body: '); + logger.verbose(req.body); + + const key = req.get('apikey'); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: InstanceDto, + execute: (instance) => instanceController.fetchInstances(instance, key), + }); + + return res.status(HttpStatus.OK).json(response); + } catch (error) { + logger.error('fetchInstances'); + logger.error(error); + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error: true, message: error.message }); + } }) .post(this.routerPath('setPresence'), ...guards, async (req, res) => { logger.verbose('request received in setPresence'); diff --git a/src/api/server.module.ts b/src/api/server.module.ts index 9b469ea76..01560cb0a 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -1,5 +1,5 @@ import { CacheEngine } from '../cache/cacheengine'; -import { configService } from '../config/env.config'; +import { configService, ProviderSession } from '../config/env.config'; import { eventEmitter } from '../config/event.config'; import { Logger } from '../config/logger.config'; import { dbserver } from '../libs/db.connect'; @@ -110,7 +110,12 @@ export const repository = new RepositoryBroker( export const cache = new CacheService(new CacheEngine(configService, 'instance').getEngine()); const chatwootCache = new CacheService(new CacheEngine(configService, ChatwootService.name).getEngine()); const baileysCache = new CacheService(new CacheEngine(configService, 'baileys').getEngine()); -const providerFiles = new ProviderFiles(configService); + +let providerFiles: ProviderFiles = null; + +if (configService.get('PROVIDER')?.ENABLED) { + providerFiles = new ProviderFiles(configService); +} export const waMonitor = new WAMonitoringService( eventEmitter, diff --git a/src/api/services/cache.service.ts b/src/api/services/cache.service.ts index caf3dbfae..e03b3eb56 100644 --- a/src/api/services/cache.service.ts +++ b/src/api/services/cache.service.ts @@ -1,4 +1,4 @@ -import { BufferJSON } from '@whiskeysockets/baileys'; +import { BufferJSON } from 'baileys'; import { Logger } from '../../config/logger.config'; import { ICache } from '../abstract/abstract.cache'; diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index ac4eae906..cf678f5cc 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -1,5 +1,5 @@ -import { WASocket } from '@whiskeysockets/baileys'; import axios from 'axios'; +import { WASocket } from 'baileys'; import { execSync } from 'child_process'; import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; @@ -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,8 +197,15 @@ 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'); + + // restart instance + if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) { + this.client.ws.close(); + this.client.ws.connect(); + } } public async findSettings() { @@ -214,6 +224,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 +233,7 @@ export class ChannelStartupService { read_messages: data.read_messages, read_status: data.read_status, sync_full_history: data.sync_full_history, + wavoipToken: data.wavoipToken, }; } @@ -686,7 +698,45 @@ export class ChannelStartupService { }); }; - public async sendDataWebhook(event: Events, data: T, local = true) { + private async retryWebhookRequest( + httpService: any, + postData: any, + baseURL: string, + isGlobal = false, + maxRetries = 10, + delaySeconds = 30, + ) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + await httpService.post('', postData); + if (attempt > 1) { + this.logger.verbose(`Webhook ${isGlobal ? 'global' : 'local'} enviado com sucesso na tentativa ${attempt}`); + } + return; + } catch (error) { + if (attempt === maxRetries) { + throw error; // Propaga o erro após todas as tentativas + } + + this.logger.warn({ + local: `${ChannelStartupService.name}.retryWebhookRequest-${isGlobal ? 'global' : 'local'}`, + message: `Tentativa ${attempt}/${maxRetries} falhou. Próxima tentativa em ${delaySeconds} segundos`, + error: error?.message, + url: baseURL, + }); + + // Aguarda o delay antes da próxima tentativa + await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)); + } + } + } + + 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; @@ -706,7 +756,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)) { @@ -721,7 +771,9 @@ export class ChannelStartupService { autoDelete: false, }); - const queueName = `${this.instanceName}.${event}`; + const eventName = event.replace(/_/g, '.').toLowerCase(); + + const queueName = `${this.instanceName}.${eventName}`; await amqp.assertQueue(queueName, { durable: true, @@ -731,7 +783,7 @@ export class ChannelStartupService { }, }); - await amqp.bindQueue(queueName, exchangeName, event); + await amqp.bindQueue(queueName, exchangeName, eventName); const message = { event, @@ -746,7 +798,10 @@ export class ChannelStartupService { message['apikey'] = instanceApikey; } - await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message))); + await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message)), { + persistent: false, + expiration: this.configService.get('RABBITMQ').MESSAGE_TTL.toString(), + }); if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { const logData = { @@ -776,6 +831,7 @@ export class ChannelStartupService { if (rabbitmqGlobal && rabbitmqEvents[we] && amqp) { const exchangeName = 'evolution_exchange'; + const prefixKey = this.configService.get('RABBITMQ').PREFIX_KEY; let retry = 0; @@ -786,7 +842,9 @@ export class ChannelStartupService { autoDelete: false, }); - const queueName = transformedWe; + const queueName = prefixKey + ? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}` + : event.replace(/_/g, '.').toLowerCase(); await amqp.assertQueue(queueName, { durable: true, @@ -796,7 +854,7 @@ export class ChannelStartupService { }, }); - await amqp.bindQueue(queueName, exchangeName, event); + await amqp.bindQueue(queueName, exchangeName, queueName); const message = { event, @@ -810,7 +868,11 @@ export class ChannelStartupService { if (expose && instanceApikey) { message['apikey'] = instanceApikey; } - await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message))); + + await amqp.publish(exchangeName, queueName, Buffer.from(JSON.stringify(message)), { + persistent: false, + expiration: this.configService.get('RABBITMQ').MESSAGE_TTL.toString(), + }); if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { const logData = { @@ -839,7 +901,7 @@ export class ChannelStartupService { } } - if (this.localSqs.enabled) { + if (this.localSqs.enabled && integration.includes('sqs')) { const sqs = getSQS(); if (sqs) { @@ -909,7 +971,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(); @@ -983,7 +1045,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; @@ -1031,12 +1093,13 @@ export class ChannelStartupService { postData['apikey'] = instanceApikey; } - await httpService.post('', postData); + await this.retryWebhookRequest(httpService, postData, baseURL); } } catch (error) { this.logger.error({ local: ChannelStartupService.name + '.sendDataWebhook-local', - message: error?.message, + message: 'Todas as tentativas de envio do webhook local falharam', + lastError: error?.message, hostName: error?.hostname, syscall: error?.syscall, code: error?.code, @@ -1050,7 +1113,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; @@ -1102,12 +1165,13 @@ export class ChannelStartupService { postData['apikey'] = globalApiKey; } - await httpService.post('', postData); + await this.retryWebhookRequest(httpService, postData, globalURL, true); } } catch (error) { this.logger.error({ local: ChannelStartupService.name + '.sendDataWebhook-global', - message: error?.message, + message: 'Todas as tentativas de envio do webhook global falharam', + lastError: error?.message, hostName: error?.hostname, syscall: error?.syscall, code: error?.code, @@ -1281,4 +1345,36 @@ export class ChannelStartupService { this.logger.verbose('Fetching chats'); return await this.repository.chat.find({ where: { owner: this.instance.name } }); } + + public async fetchContactsWithLastMessage() { + this.logger.verbose('Searching for contacts with last message'); + const contacts = await this.repository.contact.find({ where: { owner: this.instance.name } }); + const result = []; + const seenIds = new Set(); + + for (const contact of contacts) { + if (seenIds.has(contact.id)) { + continue; + } + seenIds.add(contact.id); + + const messages = await this.repository.message.find({ + where: { + owner: this.instance.name, + key: { remoteJid: contact.id }, + }, + limit: 1, + }); + if (messages && messages.length > 0) { + result.push({ + id: contact.id, + pushName: contact?.pushName ?? null, + profilePictureUrl: contact?.profilePictureUrl ?? null, + owner: contact.owner, + lastMessage: messages[0], + }); + } + } + return result; + } } 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 000000000..f03c10289 --- /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 000000000..951be1a05 --- /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 902eefdea..a09c05974 100644 --- a/src/api/services/channels/whatsapp.baileys.service.ts +++ b/src/api/services/channels/whatsapp.baileys.service.ts @@ -1,5 +1,6 @@ import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import { Boom } from '@hapi/boom'; +import axios from 'axios'; import makeWASocket, { AnyMessageContent, BufferedEventData, @@ -19,12 +20,12 @@ import makeWASocket, { GroupMetadata, isJidBroadcast, isJidGroup, + isJidNewsletter, isJidUser, makeCacheableSignalKeyStore, MessageUpsertType, MiscMessageGenerationOptions, ParticipantAction, - PHONENUMBER_MCC, prepareWAMessageMedia, proto, useMultiFileAuthState, @@ -35,16 +36,14 @@ import makeWASocket, { WAMessageUpdate, WAPresence, WASocket, -} from '@whiskeysockets/baileys'; -import { Label } from '@whiskeysockets/baileys/lib/Types/Label'; -import { LabelAssociation } from '@whiskeysockets/baileys/lib/Types/LabelAssociation'; -import axios from 'axios'; +} from 'baileys'; +import { Label } from 'baileys/lib/Types/Label'; +import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation'; import { exec } from 'child_process'; import { isBase64, isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; // import ffmpeg from 'fluent-ffmpeg'; import fs, { existsSync, readFileSync } from 'fs'; -import { parsePhoneNumber } from 'libphonenumber-js'; import Long from 'long'; import NodeCache from 'node-cache'; import { getMIMEType } from 'node-mime-types'; @@ -65,6 +64,7 @@ import { Log, ProviderSession, QrCode, + Websocket, } from '../../../config/env.config'; import { INSTANCE_DIR } from '../../../config/path.config'; import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../../exceptions'; @@ -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()); @@ -253,8 +254,8 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.verbose('Getting profile status'); const status = await this.client.fetchStatus(this.instance.wuid); - this.logger.verbose(`Profile status: ${status.status}`); - return status.status; + this.logger.verbose(`Profile status: ${status[0]?.status}`); + return status[0]?.status; } public get profilePictureUrl() { @@ -380,12 +381,6 @@ export class BaileysStartupService extends ChannelStartupService { state: connection, statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, }; - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { - instance: this.instance.name, - ...this.stateConnection, - }); } if (connection === 'close') { @@ -418,6 +413,15 @@ export class BaileysStartupService extends ChannelStartupService { this.client?.ws?.close(); this.client.end(new Error('Close connection')); this.logger.verbose('Connection closed'); + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + wuid: this.instance.wuid, + profileName: await this.getProfileName(), + profilePictureUrl: this.instance.profilePictureUrl, + ...this.stateConnection, + }); } } @@ -447,13 +451,34 @@ export class BaileysStartupService extends ChannelStartupService { { instance: this.instance.name, status: 'open', + wuid: this.instance.wuid, + profileName: await this.getProfileName(), + profilePictureUrl: this.instance.profilePictureUrl, }, ); } + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + wuid: this.instance.wuid, + profileName: await this.getProfileName(), + profilePictureUrl: this.instance.profilePictureUrl, + ...this.stateConnection, + }); } if (connection === 'connecting') { if (this.mobile) this.sendMobileCode(); + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + wuid: this.instance.wuid, + profileName: await this.getProfileName(), + profilePictureUrl: this.instance.profilePictureUrl, + ...this.stateConnection, + }); } } @@ -517,305 +542,236 @@ export class BaileysStartupService extends ChannelStartupService { return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); } - public async connectToWhatsapp(number?: string, mobile?: boolean): Promise { - this.logger.verbose('Connecting to whatsapp'); - try { - this.loadWebhook(); - this.loadChatwoot(); - this.loadSettings(); - this.loadWebsocket(); - this.loadRabbitmq(); - this.loadSqs(); - this.loadTypebot(); - this.loadProxy(); - this.loadChamaai(); + private async createClient(number?: string, mobile?: boolean): Promise { + this.instance.authState = await this.defineAuthState(); - this.instance.authState = await this.defineAuthState(); + if (!mobile) { + this.mobile = false; + } else { + this.mobile = mobile; + } - if (!mobile) { - this.mobile = false; - } else { - this.mobile = mobile; - } + const session = this.configService.get('CONFIG_SESSION_PHONE'); - const session = this.configService.get('CONFIG_SESSION_PHONE'); + let browserOptions = {}; + + if (number || this.phoneNumber) { + this.phoneNumber = number; + + this.logger.info(`Phone number: ${number}`); + } else { const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - this.logger.verbose('Browser: ' + JSON.stringify(browser)); + browserOptions = { browser }; - let version; - let log; + this.logger.info(`Browser: ${browser}`); + } - if (session.VERSION) { - version = session.VERSION.split(','); - log = `Baileys version env: ${version}`; - } else { - const baileysVersion = await fetchLatestBaileysVersion(); - version = baileysVersion.version; - log = `Baileys version: ${version}`; - } + let version; + let log; - this.logger.info(log); + if (session.VERSION) { + version = session.VERSION.split(','); + log = `Baileys version env: ${version}`; + } else { + const baileysVersion = await fetchLatestBaileysVersion(); + version = baileysVersion.version; + log = `Baileys version: ${version}`; + } - let options; + this.logger.info(log); - if (this.localProxy.enabled) { - this.logger.info('Proxy enabled: ' + this.localProxy.proxy?.host); + let options; - if (this.localProxy?.proxy?.host?.includes('proxyscrape')) { - try { - const response = await axios.get(this.localProxy.proxy?.host); - const text = response.data; - const proxyUrls = text.split('\r\n'); - const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); - const proxyUrl = 'http://' + proxyUrls[rand]; - options = { - agent: makeProxyAgent(proxyUrl), - fetchAgent: makeProxyAgent(proxyUrl), - }; - } catch (error) { - this.localProxy.enabled = false; - } - } else { + if (this.localProxy.enabled) { + this.logger.info('Proxy enabled: ' + this.localProxy.proxy?.host); + + if (this.localProxy?.proxy?.host?.includes('proxyscrape')) { + try { + const response = await axios.get(this.localProxy.proxy?.host); + const text = response.data; + const proxyUrls = text.split('\r\n'); + const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); + const proxyUrl = 'http://' + proxyUrls[rand]; options = { - agent: makeProxyAgent(this.localProxy.proxy), - fetchAgent: makeProxyAgent(this.localProxy.proxy), + agent: makeProxyAgent(proxyUrl), + fetchAgent: makeProxyAgent(proxyUrl), }; + } catch (error) { + this.localProxy.enabled = false; } + } else { + options = { + agent: makeProxyAgent(this.localProxy.proxy), + fetchAgent: makeProxyAgent(this.localProxy.proxy), + }; } + } - const socketConfig: UserFacingSocketConfig = { - ...options, - auth: { - creds: this.instance.authState.state.creds, - keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), - }, - logger: P({ level: this.logBaileys }), - printQRInTerminal: false, - mobile, - browser: number ? ['Chrome (Linux)', session.NAME, release()] : browser, - version, - markOnlineOnConnect: this.localSettings.always_online, - retryRequestDelayMs: 10, - connectTimeoutMs: 60_000, - qrTimeout: 40_000, - defaultQueryTimeoutMs: undefined, - emitOwnEvents: false, - shouldIgnoreJid: (jid) => { - const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); - const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); - - return isGroupJid || isBroadcast; - }, - msgRetryCounterCache: this.msgRetryCounterCache, - getMessage: async (key) => (await this.getMessage(key)) as Promise, - generateHighQualityLinkPreview: true, - syncFullHistory: this.localSettings.sync_full_history, - shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => { - return this.historySyncNotification(msg); - }, - userDevicesCache: this.userDevicesCache, - transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, - patchMessageBeforeSending(message) { - if ( - message.deviceSentMessage?.message?.listMessage?.listType === - proto.Message.ListMessage.ListType.PRODUCT_LIST - ) { - message = JSON.parse(JSON.stringify(message)); - - message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; - } - - if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) { - message = JSON.parse(JSON.stringify(message)); - - message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; - } - - return message; - }, - }; - - this.endSession = false; - - this.logger.verbose('Creating socket'); + const socketConfig: UserFacingSocketConfig = { + ...options, + auth: { + creds: this.instance.authState.state.creds, + keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), + }, + logger: P({ level: this.logBaileys }), + printQRInTerminal: false, + mobile, + ...browserOptions, + version, + markOnlineOnConnect: this.localSettings.always_online, + retryRequestDelayMs: 350, + maxMsgRetryCount: 4, + fireInitQueries: true, + connectTimeoutMs: 20_000, + keepAliveIntervalMs: 30_000, + qrTimeout: 45_000, + defaultQueryTimeoutMs: undefined, + emitOwnEvents: false, + shouldIgnoreJid: (jid) => { + const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); + const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); + const isNewsletter = isJidNewsletter(jid); + + return isGroupJid || isBroadcast || isNewsletter; + }, + msgRetryCounterCache: this.msgRetryCounterCache, + getMessage: async (key) => (await this.getMessage(key)) as Promise, + generateHighQualityLinkPreview: true, + syncFullHistory: this.localSettings.sync_full_history, + shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => { + return this.historySyncNotification(msg); + }, + userDevicesCache: this.userDevicesCache, + transactionOpts: { maxCommitRetries: 5, delayBetweenTriesMs: 2500 }, + patchMessageBeforeSending(message) { + if ( + message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST + ) { + message = JSON.parse(JSON.stringify(message)); - this.client = makeWASocket(socketConfig); + message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; + } - this.logger.verbose('Socket created'); + if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) { + message = JSON.parse(JSON.stringify(message)); - this.eventHandler(); + message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; + } - this.logger.verbose('Socket event handler initialized'); + return message; + }, + }; - this.phoneNumber = number; + this.endSession = false; - return this.client; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString()); - } - } + this.logger.verbose('Creating socket'); - private async sendMobileCode() { - const { registration } = this.client.authState.creds || null; + this.client = makeWASocket(socketConfig); - let phoneNumber = registration.phoneNumber || this.phoneNumber; + this.logger.verbose('Socket created'); - if (!phoneNumber.startsWith('+')) { - phoneNumber = '+' + phoneNumber; + if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) { + useVoiceCallsBaileys(this.localSettings.wavoipToken, this.client, this.connectionStatus.state as any, true); } - if (!phoneNumber) { - this.logger.error('Phone number not found'); - return; - } + this.eventHandler(); - const parsedPhoneNumber = parsePhoneNumber(phoneNumber); + this.logger.verbose('Socket event handler initialized'); - if (!parsedPhoneNumber?.isValid()) { - this.logger.error('Phone number invalid'); - return; - } + 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']); + }); - registration.phoneNumber = parsedPhoneNumber.format('E.164'); - registration.phoneNumberCountryCode = parsedPhoneNumber.countryCallingCode; - registration.phoneNumberNationalNumber = parsedPhoneNumber.nationalNumber; + 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']); + }); - const mcc = await PHONENUMBER_MCC[parsedPhoneNumber.countryCallingCode]; - if (!mcc) { - this.logger.error('MCC not found'); - return; - } + this.phoneNumber = number; - registration.phoneNumberMobileCountryCode = mcc; - registration.method = 'sms'; + return this.client; + } + public async connectToWhatsapp(number?: string, mobile?: boolean): Promise { + this.logger.verbose('Connecting to whatsapp'); try { - const response = await this.client.requestRegistrationCode(registration); - - if (['ok', 'sent'].includes(response?.status)) { - this.logger.verbose('Registration code sent successfully'); + this.loadWebhook(); + this.loadChatwoot(); + this.loadSettings(); + this.loadWebsocket(); + this.loadRabbitmq(); + this.loadSqs(); + this.loadTypebot(); + this.loadProxy(); + this.loadChamaai(); - return response; - } + return await this.createClient(number, mobile); } catch (error) { this.logger.error(error); + throw new InternalServerErrorException(error?.toString()); } } + private async sendMobileCode() { + // const { registration } = this.client.authState.creds || null; + // let phoneNumber = registration.phoneNumber || this.phoneNumber; + // if (!phoneNumber.startsWith('+')) { + // phoneNumber = '+' + phoneNumber; + // } + // if (!phoneNumber) { + // this.logger.error('Phone number not found'); + // return; + // } + // const parsedPhoneNumber = parsePhoneNumber(phoneNumber); + // if (!parsedPhoneNumber?.isValid()) { + // this.logger.error('Phone number invalid'); + // return; + // } + // registration.phoneNumber = parsedPhoneNumber.format('E.164'); + // registration.phoneNumberCountryCode = parsedPhoneNumber.countryCallingCode; + // registration.phoneNumberNationalNumber = parsedPhoneNumber.nationalNumber; + // const mcc = await PHONENUMBER_MCC[parsedPhoneNumber.countryCallingCode]; + // if (!mcc) { + // this.logger.error('MCC not found'); + // return; + // } + // registration.phoneNumberMobileCountryCode = mcc; + // registration.method = 'sms'; + // try { + // const response = await this.client.requestRegistrationCode(registration); + // if (['ok', 'sent'].includes(response?.status)) { + // this.logger.verbose('Registration code sent successfully'); + // return response; + // } + // } catch (error) { + // this.logger.error(error); + // } + } + public async receiveMobileCode(code: string) { - await this.client - .register(code.replace(/["']/g, '').trim().toLowerCase()) - .then(async () => { - this.logger.verbose('Registration code received successfully'); - }) - .catch((error) => { - this.logger.error(error); - }); + console.log(code); + // await this.client + // .register(code.replace(/["']/g, '').trim().toLowerCase()) + // .then(async () => { + // this.logger.verbose('Registration code received successfully'); + // }) + // .catch((error) => { + // this.logger.error(error); + // }); } public async reloadConnection(): Promise { try { - this.instance.authState = await this.defineAuthState(); - - const session = this.configService.get('CONFIG_SESSION_PHONE'); - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - - let version; - let log; - - if (session.VERSION) { - version = session.VERSION.split(','); - log = `Baileys version env: ${version}`; - } else { - const baileysVersion = await fetchLatestBaileysVersion(); - version = baileysVersion.version; - log = `Baileys version: ${version}`; - } - - this.logger.info(log); - - let options; - - if (this.localProxy.enabled) { - this.logger.info('Proxy enabled: ' + this.localProxy.proxy?.host); - - if (this.localProxy?.proxy?.host?.includes('proxyscrape')) { - try { - const response = await axios.get(this.localProxy.proxy?.host); - const text = response.data; - const proxyUrls = text.split('\r\n'); - const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); - const proxyUrl = 'http://' + proxyUrls[rand]; - options = { - agent: makeProxyAgent(proxyUrl), - fetchAgent: makeProxyAgent(proxyUrl), - }; - } catch (error) { - this.localProxy.enabled = false; - } - } else { - options = { - agent: makeProxyAgent(this.localProxy.proxy), - fetchAgent: makeProxyAgent(this.localProxy.proxy), - }; - } - } - - const socketConfig: UserFacingSocketConfig = { - ...options, - auth: { - creds: this.instance.authState.state.creds, - keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), - }, - logger: P({ level: this.logBaileys }), - printQRInTerminal: false, - browser: this.phoneNumber ? ['Chrome (Linux)', session.NAME, release()] : browser, - version, - markOnlineOnConnect: this.localSettings.always_online, - retryRequestDelayMs: 10, - connectTimeoutMs: 60_000, - qrTimeout: 40_000, - defaultQueryTimeoutMs: undefined, - emitOwnEvents: false, - shouldIgnoreJid: (jid) => { - const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); - const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); - - return isGroupJid || isBroadcast; - }, - msgRetryCounterCache: this.msgRetryCounterCache, - getMessage: async (key) => (await this.getMessage(key)) as Promise, - generateHighQualityLinkPreview: true, - syncFullHistory: this.localSettings.sync_full_history, - shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => { - return this.historySyncNotification(msg); - }, - userDevicesCache: this.userDevicesCache, - transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, - patchMessageBeforeSending(message) { - if ( - message.deviceSentMessage?.message?.listMessage?.listType === - proto.Message.ListMessage.ListType.PRODUCT_LIST - ) { - message = JSON.parse(JSON.stringify(message)); - - message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; - } - - if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) { - message = JSON.parse(JSON.stringify(message)); - - message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; - } - - return message; - }, - }; - - this.client = makeWASocket(socketConfig); - - return this.client; + return await this.createClient(this.phoneNumber, this.mobile); } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); @@ -977,7 +933,7 @@ export class BaileysStartupService extends ChannelStartupService { chats: Chat[]; contacts: Contact[]; messages: proto.IWebMessageInfo[]; - isLatest: boolean; + isLatest?: boolean; }, database: Database, ) => { @@ -1053,7 +1009,7 @@ export class BaileysStartupService extends ChannelStartupService { m.messageTimestamp = m.messageTimestamp?.toNumber(); } - if (m.messageTimestamp <= timestampLimitToImport) { + if ((m.messageTimestamp as number) <= timestampLimitToImport) { continue; } @@ -1097,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, @@ -1178,11 +1134,17 @@ export class BaileysStartupService extends ChannelStartupService { received?.message?.videoMessage || received?.message?.stickerMessage || received?.message?.documentMessage || + received?.message?.documentWithCaptionMessage || received?.message?.audioMessage; const contentMsg = received?.message[getContentType(received.message)] as any; - if (this.localWebhook.webhook_base64 === true && isMedia) { + if ( + (this.localWebhook.webhook_base64 === true || + (this.configService.get('WEBSOCKET').GLOBAL_EVENTS === true && + this.configService.get('WEBSOCKET').ENABLED === true)) && + isMedia + ) { const buffer = await downloadMediaMessage( { key: received.key, message: received?.message }, 'buffer', @@ -1729,7 +1691,7 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.verbose('Getting status'); return { wuid: jid, - status: (await this.client.fetchStatus(jid))?.status, + status: (await this.client.fetchStatus(jid))[0]?.status, }; } catch (error) { this.logger.verbose('Status not found'); @@ -1939,11 +1901,9 @@ export class BaileysStartupService extends ChannelStartupService { } as unknown as AnyMessageContent, { ...option, - cachedGroupMetadata: - !this.configService.get('CACHE').REDIS.ENABLED && - !this.configService.get('CACHE').LOCAL.ENABLED - ? null - : this.getGroupMetadataCache, + useCachedGroupMetadata: + !!this.configService.get('CACHE').REDIS.ENABLED && + !!this.configService.get('CACHE').LOCAL.ENABLED, } as unknown as MiscMessageGenerationOptions, ); } @@ -1959,11 +1919,9 @@ export class BaileysStartupService extends ChannelStartupService { } as unknown as AnyMessageContent, { ...option, - cachedGroupMetadata: - !this.configService.get('CACHE').REDIS.ENABLED && - !this.configService.get('CACHE').LOCAL.ENABLED - ? null - : this.getGroupMetadataCache, + useCachedGroupMetadata: + !!this.configService.get('CACHE').REDIS.ENABLED && + !!this.configService.get('CACHE').LOCAL.ENABLED, } as unknown as MiscMessageGenerationOptions, ); } @@ -1981,11 +1939,9 @@ export class BaileysStartupService extends ChannelStartupService { }, { ...option, - cachedGroupMetadata: - !this.configService.get('CACHE').REDIS.ENABLED && - !this.configService.get('CACHE').LOCAL.ENABLED - ? null - : this.getGroupMetadataCache, + useCachedGroupMetadata: + !!this.configService.get('CACHE').REDIS.ENABLED && + !!this.configService.get('CACHE').LOCAL.ENABLED, } as unknown as MiscMessageGenerationOptions, ); } @@ -2009,11 +1965,9 @@ export class BaileysStartupService extends ChannelStartupService { message as unknown as AnyMessageContent, { ...option, - cachedGroupMetadata: - !this.configService.get('CACHE').REDIS.ENABLED && - !this.configService.get('CACHE').LOCAL.ENABLED - ? null - : this.getGroupMetadataCache, + useCachedGroupMetadata: + !!this.configService.get('CACHE').REDIS.ENABLED && + !!this.configService.get('CACHE').LOCAL.ENABLED, } as unknown as MiscMessageGenerationOptions, ); })(); @@ -2031,6 +1985,32 @@ export class BaileysStartupService extends ChannelStartupService { source: getDevice(messageSent.key.id), }; + const isMedia = + messageRaw.messageType === 'imageMessage' || + messageRaw.messageType === 'videoMessage' || + messageRaw.messageType === 'documentMessage' || + messageRaw.messageType === 'audioMessage'; + + console.log('isMedia', isMedia); + + if ( + (this.localWebhook.webhook_base64 === true || + (this.configService.get('WEBSOCKET').GLOBAL_EVENTS === true && + this.configService.get('WEBSOCKET').ENABLED === true)) && + isMedia + ) { + const buffer = await downloadMediaMessage( + { key: messageRaw.key, message: messageRaw?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + }, + ); + messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined; + } + this.logger.log(messageRaw); this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); @@ -2677,7 +2657,7 @@ export class BaileysStartupService extends ChannelStartupService { const group = await this.findGroup({ groupJid: jid }, 'inner'); if (!group) { - new OnWhatsAppDto(jid, false, number); + return new OnWhatsAppDto(jid, false, number); } return new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject); @@ -3377,6 +3357,10 @@ export class BaileysStartupService extends ChannelStartupService { } public async fetchAllGroups(getParticipants: GetParticipant) { + if (this.localSettings.groups_ignore === true) { + return; + } + this.logger.verbose('Fetching all groups'); try { const fetch = Object.values(await this.client.groupFetchAllParticipating()); diff --git a/src/api/services/channels/whatsapp.business.service.ts b/src/api/services/channels/whatsapp.business.service.ts index 861786598..0bf73dbc7 100644 --- a/src/api/services/channels/whatsapp.business.service.ts +++ b/src/api/services/channels/whatsapp.business.service.ts @@ -743,6 +743,7 @@ export class BusinessStartupService extends ChannelStartupService { [message['type']]: message['id'], preview_url: linkPreview, caption: message['caption'], + filename: message['fileName'], }, }; quoted ? (content.context = { message_id: quoted.id }) : content; @@ -1212,7 +1213,7 @@ export class BusinessStartupService extends ChannelStartupService { try { const msg = data.message; this.logger.verbose('Getting base64 from media message'); - const messageType = msg.messageType + 'Message'; + const messageType = msg.messageType.includes('Message') ? msg.messageType : msg.messageType + 'Message'; const mediaMessage = msg.message[messageType]; this.logger.verbose('Media message downloaded'); diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts index 101b005eb..9b7809d9e 100644 --- a/src/api/services/monitor.service.ts +++ b/src/api/services/monitor.service.ts @@ -323,7 +323,7 @@ export class WAMonitoringService { this.logger.verbose('Loading instances'); try { - if (this.providerSession.ENABLED) { + if (this.providerSession?.ENABLED) { await this.loadInstancesFromProvider(); } else if (this.redis.REDIS.ENABLED && this.redis.REDIS.SAVE_INSTANCES) { await this.loadInstancesFromRedis(); diff --git a/src/api/types/wa.types.ts b/src/api/types/wa.types.ts index 9c33ac6fa..e380d640d 100644 --- a/src/api/types/wa.types.ts +++ b/src/api/types/wa.types.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import { AuthenticationState, WAConnectionState } from '@whiskeysockets/baileys'; +import { AuthenticationState, WAConnectionState } from 'baileys'; export enum Events { APPLICATION_STARTUP = 'application.startup', @@ -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/cache/rediscache.ts b/src/cache/rediscache.ts index 6e209ef11..c4e98968d 100644 --- a/src/cache/rediscache.ts +++ b/src/cache/rediscache.ts @@ -1,4 +1,4 @@ -import { BufferJSON } from '@whiskeysockets/baileys'; +import { BufferJSON } from 'baileys'; import { RedisClientType } from 'redis'; import { ICache } from '../api/abstract/abstract.cache'; diff --git a/src/config/env.config.ts b/src/config/env.config.ts index ddd5ce9f4..b7bccdb90 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -104,7 +104,11 @@ export type Rabbitmq = { ENABLED: boolean; URI: string; EXCHANGE_NAME: string; + PREFIX_KEY?: string; GLOBAL_ENABLED: boolean; + MESSAGE_TTL: number; + MAX_LENGTH: number; + MAX_LENGTH_BYTES: number; EVENTS: EventsRabbitmq; }; @@ -323,7 +327,11 @@ export class ConfigService { ENABLED: process.env?.RABBITMQ_ENABLED === 'true', GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true', EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange', + PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY || '', URI: process.env.RABBITMQ_URI || '', + MESSAGE_TTL: Number.parseInt(process.env?.RABBITMQ_MESSAGE_TTL) || 604800, + MAX_LENGTH: Number.parseInt(process.env?.RABBITMQ_MAX_LENGTH) || 10000, + MAX_LENGTH_BYTES: Number.parseInt(process.env?.RABBITMQ_MAX_LENGTH_BYTES) || 8192, EVENTS: { APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true', INSTANCE_CREATE: process.env?.RABBITMQ_EVENTS_INSTANCE_CREATE === 'true', @@ -366,9 +374,9 @@ export class ConfigService { GLOBAL_EVENTS: process.env?.WEBSOCKET_GLOBAL_EVENTS === 'true', }, WA_BUSINESS: { - TOKEN_WEBHOOK: process.env.WA_BUSINESS_TOKEN_WEBHOOK || '', - URL: process.env.WA_BUSINESS_URL || '', - VERSION: process.env.WA_BUSINESS_VERSION || '', + TOKEN_WEBHOOK: process.env.WA_BUSINESS_TOKEN_WEBHOOK || 'evolution', + URL: process.env.WA_BUSINESS_URL || 'https://graph.facebook.com', + VERSION: process.env.WA_BUSINESS_VERSION || 'v19.0', LANGUAGE: process.env.WA_BUSINESS_LANGUAGE || 'en', }, LOG: { diff --git a/src/dev-env.yml b/src/dev-env.yml index 42573ef35..3630702a7 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -89,7 +89,14 @@ RABBITMQ: ENABLED: false URI: "amqp://guest:guest@localhost:5672" EXCHANGE_NAME: evolution_exchange + PREFIX_KEY: evolution GLOBAL_ENABLED: true + # Tempo de vida das mensagens: 1 hora em milissegundos (3600000 = 60 * 60 * 1000) + MESSAGE_TTL: 3600000 + # Limite máximo de mensagens por fila (quando atingido, novas mensagens são rejeitadas) + MAX_LENGTH: 1000 + # Tamanho máximo em bytes permitido para filas: 10MB (10485760 = 10 * 1024 * 1024) + MAX_LENGTH_BYTES: 10485760 EVENTS: APPLICATION_STARTUP: false INSTANCE_CREATE: false diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 59b252d3e..8b73c5d9e 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -25,7 +25,7 @@ info: [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/26869335-5546d063-156b-4529-915f-909dd628c090?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D26869335-5546d063-156b-4529-915f-909dd628c090%26entityType%3Dcollection%26workspaceId%3D339a4ee7-378b-45c9-b5b8-fd2c0a9c2442) - version: 1.8.0 + version: 1.8.2 contact: name: DavidsonGomes email: contato@agenciadgcode.com diff --git a/src/main.ts b/src/main.ts index 2cc9e2806..366509e50 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,7 @@ import { initIO } from './api/integrations/websocket/libs/socket.server'; import { ProviderFiles } from './api/provider/sessions'; import { HttpStatus, router } from './api/routes/index.router'; import { waMonitor } from './api/server.module'; -import { Auth, configService, Cors, HttpServer, Rabbitmq, Sqs, Webhook } from './config/env.config'; +import { Auth, configService, Cors, HttpServer, ProviderSession, Rabbitmq, Sqs, Webhook } from './config/env.config'; import { onUnexpectedError } from './config/error.config'; import { Logger } from './config/logger.config'; import { ROOT_DIR } from './config/path.config'; @@ -27,9 +27,13 @@ async function bootstrap() { const logger = new Logger('SERVER'); const app = express(); - const providerFiles = new ProviderFiles(configService); - await providerFiles.onModuleInit(); - logger.info('Provider:Files - ON'); + let providerFiles: ProviderFiles = null; + + if (configService.get('PROVIDER')?.ENABLED) { + providerFiles = new ProviderFiles(configService); + await providerFiles.onModuleInit(); + logger.info('Provider:Files - ON'); + } app.use( cors({ diff --git a/src/utils/use-multi-file-auth-state-db.ts b/src/utils/use-multi-file-auth-state-db.ts index 995ac92ad..dd25270e8 100644 --- a/src/utils/use-multi-file-auth-state-db.ts +++ b/src/utils/use-multi-file-auth-state-db.ts @@ -1,29 +1,55 @@ -import { - AuthenticationCreds, - AuthenticationState, - BufferJSON, - initAuthCreds, - proto, - SignalDataTypeMap, -} from '@whiskeysockets/baileys'; +import { AuthenticationState, BufferJSON, initAuthCreds, WAProto as proto } from 'baileys'; +import fs from 'fs/promises'; +import path from 'path'; import { configService, Database } from '../config/env.config'; import { Logger } from '../config/logger.config'; +import { INSTANCE_DIR } from '../config/path.config'; import { dbserver } from '../libs/db.connect'; +const fixFileName = (file) => { + if (!file) { + return undefined; + } + const replacedSlash = file.replace(/\//g, '__'); + const replacedColon = replacedSlash.replace(/:/g, '-'); + return replacedColon; +}; + +async function fileExists(file) { + try { + const stat = await fs.stat(file); + if (stat.isFile()) return true; + } catch (error) { + return; + } +} + export async function useMultiFileAuthStateDb( coll: string, ): Promise<{ state: AuthenticationState; saveCreds: () => Promise }> { - const logger = new Logger(useMultiFileAuthStateDb.name); - const client = dbserver.getClient(); + const logger = new Logger(useMultiFileAuthStateDb.name); + const collection = client .db(configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') .collection(coll); - const writeData = async (data: any, key: string): Promise => { + const sessionId = coll; + + const localFolder = path.join(INSTANCE_DIR, sessionId); + const localFile = (key: string) => path.join(localFolder, fixFileName(key) + '.json'); + await fs.mkdir(localFolder, { recursive: true }); + + async function writeData(data: any, key: string): Promise { try { + const dataString = JSON.stringify(data, BufferJSON.replacer); + + if (key != 'creds') { + await fs.writeFile(localFile(key), dataString); + return; + } await client.connect(); let msgParsed = JSON.parse(JSON.stringify(data, BufferJSON.replacer)); if (Array.isArray(msgParsed)) { @@ -37,42 +63,59 @@ export async function useMultiFileAuthStateDb( }); } catch (error) { logger.error(error); + return; } - }; + } - const readData = async (key: string): Promise => { + async function readData(key: string): Promise { try { - await client.connect(); - let data = (await collection.findOne({ _id: key })) as any; - if (data?.content_array) { - data = data.content_array; + if (key != 'creds') { + if (!(await fileExists(localFile(key)))) return null; + const rawData = await fs.readFile(localFile(key), { encoding: 'utf-8' }); + + const parsedData = JSON.parse(rawData, BufferJSON.reviver); + return parsedData; + } else { + await client.connect(); + let data = (await collection.findOne({ _id: key })) as any; + if (data?.content_array) { + data = data.content_array; + } + const creds = JSON.stringify(data); + return JSON.parse(creds, BufferJSON.reviver); } - const creds = JSON.stringify(data); - return JSON.parse(creds, BufferJSON.reviver); } catch (error) { logger.error(error); + return null; } - }; + } - const removeData = async (key: string) => { + async function removeData(key: string): Promise { try { - await client.connect(); - return await collection.deleteOne({ _id: key }); + if (key != 'creds') { + await fs.unlink(localFile(key)); + } else { + await client.connect(); + return await collection.deleteOne({ _id: key }); + } } catch (error) { logger.error(error); + return; } - }; + } - const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); + let creds = await readData('creds'); + if (!creds) { + creds = initAuthCreds(); + await writeData(creds, 'creds'); + } return { state: { creds, keys: { - get: async (type, ids: string[]) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const data: { [_: string]: SignalDataTypeMap[type] } = {}; + get: async (type, ids) => { + const data = {}; await Promise.all( ids.map(async (id) => { let value = await readData(`${type}-${id}`); @@ -83,25 +126,24 @@ export async function useMultiFileAuthStateDb( data[id] = value; }), ); - return data; }, - set: async (data: any) => { - const tasks: Promise[] = []; + set: async (data) => { + const tasks = []; for (const category in data) { for (const id in data[category]) { const value = data[category][id]; const key = `${category}-${id}`; + tasks.push(value ? writeData(value, key) : removeData(key)); } } - await Promise.all(tasks); }, }, }, - saveCreds: async () => { - return await writeData(creds, 'creds'); + saveCreds: () => { + return writeData(creds, 'creds'); }, }; } diff --git a/src/utils/use-multi-file-auth-state-provider-files.ts b/src/utils/use-multi-file-auth-state-provider-files.ts index 1051af8f7..ec4d7e6c4 100644 --- a/src/utils/use-multi-file-auth-state-provider-files.ts +++ b/src/utils/use-multi-file-auth-state-provider-files.ts @@ -34,14 +34,7 @@ * └──────────────────────────────────────────────────────────────────────────────┘ */ -import { - AuthenticationCreds, - AuthenticationState, - BufferJSON, - initAuthCreds, - proto, - SignalDataTypeMap, -} from '@whiskeysockets/baileys'; +import { AuthenticationCreds, AuthenticationState, BufferJSON, initAuthCreds, proto, SignalDataTypeMap } from 'baileys'; import { isNotEmpty } from 'class-validator'; import { ProviderFiles } from '../api/provider/sessions'; diff --git a/src/utils/use-multi-file-auth-state-redis-db.ts b/src/utils/use-multi-file-auth-state-redis-db.ts index 66bb89ea3..d077b894e 100644 --- a/src/utils/use-multi-file-auth-state-redis-db.ts +++ b/src/utils/use-multi-file-auth-state-redis-db.ts @@ -1,10 +1,4 @@ -import { - AuthenticationCreds, - AuthenticationState, - initAuthCreds, - proto, - SignalDataTypeMap, -} from '@whiskeysockets/baileys'; +import { AuthenticationCreds, AuthenticationState, initAuthCreds, proto, SignalDataTypeMap } from 'baileys'; import { CacheService } from '../api/services/cache.service'; import { Logger } from '../config/logger.config'; diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 8f7cb1a0e..e356342c5 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 = {