diff --git a/Dockerfile b/Dockerfile index 24c4e3bc7..31b399c43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,17 @@ -FROM node:24-alpine AS builder +FROM node:20-alpine AS builder RUN apk update && \ - apk add --no-cache git ffmpeg wget curl bash openssl + apk add git ffmpeg wget curl bash -LABEL version="2.3.1" description="Api to control whatsapp features through http requests." +LABEL version="2.2.0" description="Api to control whatsapp features through http requests." LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" -LABEL contact="contato@evolution-api.com" +LABEL contact="contato@atendai.com" WORKDIR /evolution -COPY ./package*.json ./ -COPY ./tsconfig.json ./ -COPY ./tsup.config.ts ./ +COPY ./package.json ./tsconfig.json ./ -RUN npm ci --silent +RUN npm install -f COPY ./src ./src COPY ./public ./public @@ -21,6 +19,7 @@ COPY ./prisma ./prisma COPY ./manager ./manager COPY ./.env.example ./.env COPY ./runWithProvider.js ./ +COPY ./tsup.config.ts ./ COPY ./Docker ./Docker @@ -28,15 +27,14 @@ RUN chmod +x ./Docker/scripts/* && dos2unix ./Docker/scripts/* RUN ./Docker/scripts/generate_database.sh -RUN npm run build +RUN ./node_modules/.bin/tsup -FROM node:24-alpine AS final +FROM node:20-alpine AS final RUN apk update && \ - apk add tzdata ffmpeg bash openssl + apk add tzdata ffmpeg bash openssl openssl-dev libc6-compat -ENV TZ=America/Sao_Paulo -ENV DOCKER_ENV=true +ENV TZ=America/Fortaleza WORKDIR /evolution @@ -57,4 +55,4 @@ ENV DOCKER_ENV=true EXPOSE 8080 -ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ] +ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index e0edee656..554a37ba4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,84 +1,60 @@ -version: "3.8" - services: api: container_name: evolution_api - image: evoapicloud/evolution-api:latest + build: + context: . + dockerfile: Dockerfile restart: always depends_on: - redis - - evolution-postgres + - postgres ports: - - "127.0.0.1:8080:8080" + - 8080:8080 volumes: - evolution_instances:/evolution/instances networks: - evolution-net - - dokploy-network env_file: - .env expose: - - "8080" - - frontend: - container_name: evolution_frontend - image: evoapicloud/evolution-manager:latest - restart: always - ports: - - "3000:80" - networks: - - evolution-net + - 8080 redis: - container_name: evolution_redis image: redis:latest - restart: always + networks: + - evolution-net + container_name: redis command: > redis-server --port 6379 --appendonly yes volumes: - evolution_redis:/data - networks: - evolution-net: - aliases: - - evolution-redis - dokploy-network: - aliases: - - evolution-redis - expose: - - "6379" + ports: + - 6379:6379 + restart: always - evolution-postgres: - container_name: evolution_postgres + postgres: + container_name: postgres image: postgres:15 + networks: + - evolution-net + command: ["postgres", "-c", "max_connections=1000"] restart: always - env_file: - - .env - command: - - postgres - - -c - - max_connections=1000 - - -c - - listen_addresses=* + ports: + - 5432:5432 environment: - - POSTGRES_DB=${POSTGRES_DATABASE} - - POSTGRES_USER=${POSTGRES_USERNAME} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data - networks: - - evolution-net - - dokploy-network expose: - - "5432" + - 5432 volumes: evolution_instances: evolution_redis: postgres_data: + networks: evolution-net: name: evolution-net driver: bridge - dokploy-network: - external: true \ No newline at end of file diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..101deb1fa 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1,4 +1,3 @@ -import { getCollectionsDto } from '@api/dto/business.dto'; import { OfferCallDto } from '@api/dto/call.dto'; import { ArchiveChatDto, @@ -55,14 +54,13 @@ import { import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper'; import * as s3Service from '@api/integrations/storage/s3/libs/minio.server'; import { ProviderFiles } from '@api/provider/sessions'; -import { PrismaRepository, Query } from '@api/repository/repository.service'; +import { PrismaRepository } from '@api/repository/repository.service'; import { chatbotController, waMonitor } from '@api/server.module'; import { CacheService } from '@api/services/cache.service'; import { ChannelStartupService } from '@api/services/channel.service'; -import { Events, MessageSubtype, TypeMediaMessage, wa } from '@api/types/wa.types'; +import { Events, LidContact, LidMessageKey, MessageSubtype, resolveLidContact, resolveLidJid, TypeMediaMessage, wa } from '@api/types/wa.types'; import { CacheEngine } from '@cache/cacheengine'; import { - AudioConverter, CacheConf, Chatwoot, ConfigService, @@ -78,14 +76,11 @@ import { import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions'; import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import { Boom } from '@hapi/boom'; -import { createId as cuid } from '@paralleldrive/cuid2'; -import { Instance, Message } from '@prisma/client'; +import { Instance } from '@prisma/client'; import { createJid } from '@utils/createJid'; -import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; -import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; +import { makeProxyAgent } from '@utils/makeProxyAgent'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import { status } from '@utils/renderStatus'; -import { sendTelemetry } from '@utils/sendTelemetry'; import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; @@ -95,39 +90,34 @@ import makeWASocket, { BufferedEventData, BufferJSON, CacheStore, - CatalogCollection, Chat, ConnectionState, Contact, - decryptPollVote, delay, DisconnectReason, - downloadContentFromMessage, downloadMediaMessage, + fetchLatestBaileysVersion, generateWAMessageFromContent, getAggregateVotesInPollMessage, - GetCatalogOptions, getContentType, getDevice, GroupMetadata, isJidBroadcast, isJidGroup, isJidNewsletter, - isPnUser, - jidNormalizedUser, + isJidUser, makeCacheableSignalKeyStore, MessageUpsertType, MessageUserReceiptUpdate, MiscMessageGenerationOptions, ParticipantAction, prepareWAMessageMedia, - Product, proto, UserFacingSocketConfig, WABrowserDescription, WAMediaUpload, WAMessage, - WAMessageKey, + WAMessageUpdate, WAPresence, WASocket, } from 'baileys'; @@ -135,12 +125,13 @@ import { Label } from 'baileys/lib/Types/Label'; import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation'; import { spawn } from 'child_process'; import { isArray, isBase64, isURL } from 'class-validator'; -import { createHash } from 'crypto'; +import { randomBytes } from 'crypto'; import EventEmitter2 from 'eventemitter2'; import ffmpeg from 'fluent-ffmpeg'; import FormData from 'form-data'; +import { readFileSync } from 'fs'; import Long from 'long'; -import mimeTypes from 'mime-types'; +import mime from 'mime'; import NodeCache from 'node-cache'; import cron from 'node-cron'; import { release } from 'os'; @@ -152,16 +143,6 @@ import sharp from 'sharp'; import { PassThrough, Readable } from 'stream'; import { v4 } from 'uuid'; -import { BaileysMessageProcessor } from './baileysMessage.processor'; -import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; - -export interface ExtendedIMessageKey extends proto.IMessageKey { - remoteJidAlt?: string; - participantAlt?: string; - server_id?: string; - isViewOnce?: boolean; -} - const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine()); // Adicione a função getVideoDuration no início do arquivo @@ -225,8 +206,6 @@ async function getVideoDuration(input: Buffer | string | Readable): Promise('LOG').BAILEYS; - private eventProcessingQueue: Promise = Promise.resolve(); - - // Cache TTL constants (in seconds) - private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing - private readonly UPDATE_CACHE_TTL_SECONDS = 30 * 60; // 30 minutes - avoid duplicate status updates public stateConnection: wa.StateConnection = { state: 'close' }; @@ -265,43 +236,28 @@ export class BaileysStartupService extends ChannelStartupService { } public async logoutInstance() { - this.messageProcessor.onDestroy(); await this.client?.logout('Log out instance: ' + this.instanceName); this.client?.ws?.close(); - const db = this.configService.get('DATABASE'); - const cache = this.configService.get('CACHE'); - const provider = this.configService.get('PROVIDER'); - - if (provider?.ENABLED) { - const authState = await this.authStateProvider.authStateProvider(this.instance.id); - - await authState.removeCreds(); - } - - if (cache?.REDIS.ENABLED && cache?.REDIS.SAVE_INSTANCES) { - const authState = await useMultiFileAuthStateRedisDb(this.instance.id, this.cache); - - await authState.removeCreds(); - } - - if (db.SAVE_DATA.INSTANCE) { - const authState = await useMultiFileAuthStatePrisma(this.instance.id, this.cache); - - await authState.removeCreds(); - } - - const sessionExists = await this.prismaRepository.session.findFirst({ where: { sessionId: this.instanceId } }); + const sessionExists = await this.prismaRepository.session.findFirst({ + where: { sessionId: this.instanceId }, + }); if (sessionExists) { - await this.prismaRepository.session.delete({ where: { sessionId: this.instanceId } }); + await this.prismaRepository.session.delete({ + where: { + sessionId: this.instanceId, + }, + }); } } public async getProfileName() { let profileName = this.client.user?.name ?? this.client.user?.verifiedName; if (!profileName) { - const data = await this.prismaRepository.session.findUnique({ where: { sessionId: this.instanceId } }); + const data = await this.prismaRepository.session.findUnique({ + where: { sessionId: this.instanceId }, + }); if (data) { const creds = JSON.parse(JSON.stringify(data.creds), BufferJSON.reviver); @@ -315,7 +271,7 @@ export class BaileysStartupService extends ChannelStartupService { public async getProfileStatus() { const status = await this.client.fetchStatus(this.instance.wuid); - return status[0]?.status; + return status?.status; } public get profilePictureUrl() { @@ -343,7 +299,10 @@ export class BaileysStartupService extends ChannelStartupService { this.chatwootService.eventWhatsapp( Events.QRCODE_UPDATED, { instanceName: this.instance.name, instanceId: this.instanceId }, - { message: 'QR code limit reached, please login again', statusCode: DisconnectReason.badSession }, + { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, + }, ); } @@ -351,9 +310,6 @@ export class BaileysStartupService extends ChannelStartupService { instance: this.instance.name, state: 'refused', statusReason: DisconnectReason.connectionClosed, - wuid: this.instance.wuid, - profileName: await this.getProfileName(), - profilePictureUrl: this.instance.profilePictureUrl, }); this.endSession = true; @@ -389,7 +345,12 @@ export class BaileysStartupService extends ChannelStartupService { this.instance.qrcode.code = qr; this.sendDataWebhook(Events.QRCODE_UPDATED, { - qrcode: { instance: this.instance.name, pairingCode: this.instance.qrcode.pairingCode, code: qr, base64 }, + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, }); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { @@ -397,7 +358,12 @@ export class BaileysStartupService extends ChannelStartupService { Events.QRCODE_UPDATED, { instanceName: this.instance.name, instanceId: this.instanceId }, { - qrcode: { instance: this.instance.name, pairingCode: this.instance.qrcode.pairingCode, code: qr, base64 }, + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, }, ); } @@ -412,7 +378,9 @@ export class BaileysStartupService extends ChannelStartupService { await this.prismaRepository.instance.update({ where: { id: this.instanceId }, - data: { connectionStatus: 'connecting' }, + data: { + connectionStatus: 'connecting', + }, }); } @@ -421,10 +389,23 @@ export class BaileysStartupService extends ChannelStartupService { state: connection, statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, }; + + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + ...this.stateConnection, + }); } if (connection === 'close') { const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; + + // Guard: if connection closed before QR was scanned (no wuid, no error code), do not reconnect + // Without this guard, a premature close triggers infinite QR regeneration loop + if (!this.instance.wuid && !statusCode) { + this.logger.info('Connection closed before QR scan — skipping reconnect to prevent loop'); + return; + } + const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406]; const shouldReconnect = !codesToNotReconnect.includes(statusCode); if (shouldReconnect) { @@ -452,15 +433,16 @@ export class BaileysStartupService extends ChannelStartupService { this.chatwootService.eventWhatsapp( Events.STATUS_INSTANCE, { instanceName: this.instance.name, instanceId: this.instanceId }, - { instance: this.instance.name, status: 'closed' }, + { + instance: this.instance.name, + status: 'closed', + }, ); } this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); this.client?.ws?.close(); this.client.end(new Error('Close connection')); - - this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, ...this.stateConnection }); } } @@ -469,7 +451,7 @@ export class BaileysStartupService extends ChannelStartupService { try { const profilePic = await this.profilePicture(this.instance.wuid); this.instance.profilePictureUrl = profilePic.profilePictureUrl; - } catch { + } catch (error) { this.instance.profilePictureUrl = null; } const formattedWuid = this.instance.wuid.split('@')[0].padEnd(30, ' '); @@ -501,34 +483,27 @@ export class BaileysStartupService extends ChannelStartupService { this.chatwootService.eventWhatsapp( Events.CONNECTION_UPDATE, { instanceName: this.instance.name, instanceId: this.instanceId }, - { instance: this.instance.name, status: 'open' }, + { + instance: this.instance.name, + status: 'open', + }, ); this.syncChatwootLostMessages(); } - - 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') { - this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, ...this.stateConnection }); } } private async getMessage(key: proto.IMessageKey, full = false) { try { - // Use raw SQL to avoid JSON path issues - const webMessageInfo = (await this.prismaRepository.$queryRaw` - SELECT * FROM "Message" - WHERE "instanceId" = ${this.instanceId} - AND "key"->>'id' = ${key.id} - `) as proto.IWebMessageInfo[]; - + const webMessageInfo = (await this.prismaRepository.message.findMany({ + where: { + instanceId: this.instanceId, + key: { + path: ['id'], + equals: key.id, + }, + }, + })) as unknown as proto.IWebMessageInfo[]; if (full) { return webMessageInfo[0]; } @@ -539,7 +514,9 @@ export class BaileysStartupService extends ChannelStartupService { const messageSecret = Buffer.from(messageSecretBase64, 'base64'); const msg = { - messageContextInfo: { messageSecret }, + messageContextInfo: { + messageSecret, + }, pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, }; @@ -548,7 +525,7 @@ export class BaileysStartupService extends ChannelStartupService { } return webMessageInfo[0].message; - } catch { + } catch (error) { return { conversation: '' }; } } @@ -591,9 +568,17 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.info(`Browser: ${browser}`); } - const baileysVersion = await fetchLatestWaWebVersion({}); - const version = baileysVersion.version; - const log = `Baileys version: ${version.join('.')}`; + 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); @@ -611,8 +596,11 @@ export class BaileysStartupService extends ChannelStartupService { 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: makeProxyAgentUndici(proxyUrl) }; - } catch { + options = { + agent: makeProxyAgent(proxyUrl), + fetchAgent: makeProxyAgent(proxyUrl), + }; + } catch (error) { this.localProxy.enabled = false; } } else { @@ -624,7 +612,7 @@ export class BaileysStartupService extends ChannelStartupService { username: this.localProxy.username, password: this.localProxy.password, }), - fetchAgent: makeProxyAgentUndici({ + fetchAgent: makeProxyAgent({ host: this.localProxy.host, port: this.localProxy.port, protocol: this.localProxy.protocol, @@ -657,10 +645,6 @@ export class BaileysStartupService extends ChannelStartupService { qrTimeout: 45_000, emitOwnEvents: false, shouldIgnoreJid: (jid) => { - if (this.localSettings.syncFullHistory && isJidGroup(jid)) { - return false; - } - const isGroupJid = this.localSettings.groupsIgnore && isJidGroup(jid); const isBroadcast = !this.localSettings.readStatus && isJidBroadcast(jid); const isNewsletter = isJidNewsletter(jid); @@ -697,24 +681,8 @@ 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; @@ -727,11 +695,6 @@ export class BaileysStartupService extends ChannelStartupService { this.loadWebhook(); this.loadProxy(); - // Remontar o messageProcessor para garantir que está funcionando após reconexão - this.messageProcessor.mount({ - onMessageReceive: this.messageHandle['messages.upsert'].bind(this), - }); - return await this.createClient(number); } catch (error) { this.logger.error(error); @@ -770,13 +733,18 @@ export class BaileysStartupService extends ChannelStartupService { if (chatsToInsert.length > 0) { if (this.configService.get('DATABASE').SAVE_DATA.CHATS) - await this.prismaRepository.chat.createMany({ data: chatsToInsert, skipDuplicates: true }); + await this.prismaRepository.chat.createMany({ + data: chatsToInsert, + skipDuplicates: true, + }); } }, 'chats.update': async ( chats: Partial< - proto.IConversation & { lastMessageRecvTimestamp?: number } & { + proto.IConversation & { + lastMessageRecvTimestamp?: number; + } & { conditional: (bufferedData: BufferedEventData) => boolean; } >[], @@ -789,7 +757,11 @@ export class BaileysStartupService extends ChannelStartupService { for (const chat of chats) { await this.prismaRepository.chat.updateMany({ - where: { instanceId: this.instanceId, remoteJid: chat.id, name: chat.name }, + where: { + instanceId: this.instanceId, + remoteJid: chat.id, + name: chat.name, + }, data: { remoteJid: chat.id }, }); } @@ -798,7 +770,9 @@ export class BaileysStartupService extends ChannelStartupService { 'chats.delete': async (chats: string[]) => { chats.forEach( async (chat) => - await this.prismaRepository.chat.deleteMany({ where: { instanceId: this.instanceId, remoteJid: chat } }), + await this.prismaRepository.chat.deleteMany({ + where: { instanceId: this.instanceId, remoteJid: chat }, + }), ); this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); @@ -806,10 +780,10 @@ export class BaileysStartupService extends ChannelStartupService { }; private readonly contactHandle = { - 'contacts.upsert': async (contacts: Contact[]) => { + 'contacts.upsert': async (contacts: LidContact[]) => { try { const contactsRaw: any = contacts.map((contact) => ({ - remoteJid: contact.id, + remoteJid: resolveLidContact(contact), pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], profilePicUrl: null, instanceId: this.instanceId, @@ -819,7 +793,10 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); if (this.configService.get('DATABASE').SAVE_DATA.CONTACTS) - await this.prismaRepository.contact.createMany({ data: contactsRaw, skipDuplicates: true }); + await this.prismaRepository.contact.createMany({ + data: contactsRaw, + skipDuplicates: true, + }); const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp')); if (usersContacts) { @@ -844,8 +821,8 @@ export class BaileysStartupService extends ChannelStartupService { } const updatedContacts = await Promise.all( - contacts.map(async (contact) => ({ - remoteJid: contact.id, + contacts.map(async (contact: LidContact) => ({ + remoteJid: resolveLidContact(contact), pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], profilePicUrl: (await this.profilePicture(contact.id)).profilePictureUrl, instanceId: this.instanceId, @@ -861,12 +838,12 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.CONTACTS_UPDATE, updatedContacts); await Promise.all( updatedContacts.map(async (contact) => { - if (this.configService.get('DATABASE').SAVE_DATA.CONTACTS) { - await this.prismaRepository.contact.updateMany({ - where: { remoteJid: contact.remoteJid, instanceId: this.instanceId }, - data: { profilePicUrl: contact.profilePicUrl }, - }); - } + const update = this.prismaRepository.contact.updateMany({ + where: { remoteJid: contact.remoteJid, instanceId: this.instanceId }, + data: { + profilePicUrl: contact.profilePicUrl, + }, + }); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { const instance = { instanceName: this.instance.name, instanceId: this.instance.id }; @@ -885,6 +862,8 @@ export class BaileysStartupService extends ChannelStartupService { avatar_url: contact.profilePicUrl, }); } + + return update; }), ); } @@ -895,9 +874,13 @@ export class BaileysStartupService extends ChannelStartupService { }, 'contacts.update': async (contacts: Partial[]) => { - const contactsRaw: { remoteJid: string; pushName?: string; profilePicUrl?: string; instanceId: string }[] = []; + const contactsRaw: { + remoteJid: string; + pushName?: string; + profilePicUrl?: string; + instanceId: string; + }[] = []; for await (const contact of contacts) { - this.logger.debug(`Updating contact: ${JSON.stringify(contact, null, 2)}`); contactsRaw.push({ remoteJid: contact.id, pushName: contact?.name ?? contact?.verifiedName, @@ -908,18 +891,19 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); - if (this.configService.get('DATABASE').SAVE_DATA.CONTACTS) { - const updateTransactions = contactsRaw.map((contact) => - this.prismaRepository.contact.upsert({ - where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } }, - create: contact, - update: contact, - }), - ); - await this.prismaRepository.$transaction(updateTransactions); - } + const updateTransactions = contactsRaw.map((contact) => + this.prismaRepository.contact.upsert({ + where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } }, + create: contact, + update: contact, + }), + ); + await this.prismaRepository.$transaction(updateTransactions); - //const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp')); + const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp')); + if (usersContacts) { + await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid }))); + } }, }; @@ -934,7 +918,7 @@ export class BaileysStartupService extends ChannelStartupService { }: { chats: Chat[]; contacts: Contact[]; - messages: WAMessage[]; + messages: proto.IWebMessageInfo[]; isLatest?: boolean; progress?: number; syncType?: proto.HistorySync.HistorySyncType; @@ -966,19 +950,13 @@ export class BaileysStartupService extends ChannelStartupService { } } - const contactsMap = new Map(); - - for (const contact of contacts) { - if (contact.id && (contact.notify || contact.name)) { - contactsMap.set(contact.id, { name: contact.name ?? contact.notify, jid: contact.id }); - } - } - const chatsRaw: { remoteJid: string; instanceId: string; name?: string }[] = []; const chatsRepository = new Set( - (await this.prismaRepository.chat.findMany({ where: { instanceId: this.instanceId } })).map( - (chat) => chat.remoteJid, - ), + ( + await this.prismaRepository.chat.findMany({ + where: { instanceId: this.instanceId }, + }) + ).map((chat) => chat.remoteJid), ); for (const chat of chats) { @@ -986,18 +964,25 @@ export class BaileysStartupService extends ChannelStartupService { continue; } - chatsRaw.push({ remoteJid: chat.id, instanceId: this.instanceId, name: chat.name }); + chatsRaw.push({ + remoteJid: chat.id, + instanceId: this.instanceId, + name: chat.name, + }); } this.sendDataWebhook(Events.CHATS_SET, chatsRaw); if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { - await this.prismaRepository.chat.createMany({ data: chatsRaw, skipDuplicates: true }); + await this.prismaRepository.chat.createMany({ + data: chatsRaw, + skipDuplicates: true, + }); } const messagesRaw: any[] = []; - const messagesRepository: Set = new Set( + const messagesRepository = new Set( chatwootImport.getRepositoryMessagesCache(instance) ?? ( await this.prismaRepository.message.findMany({ @@ -1005,7 +990,9 @@ export class BaileysStartupService extends ChannelStartupService { where: { instanceId: this.instanceId }, }) ).map((message) => { - const key = message.key as { id: string }; + const key = message.key as { + id: string; + }; return key.id; }), @@ -1034,25 +1021,16 @@ export class BaileysStartupService extends ChannelStartupService { continue; } - if (!m.pushName && !m.key.fromMe) { - const participantJid = m.participant || m.key.participant || m.key.remoteJid; - if (participantJid && contactsMap.has(participantJid)) { - m.pushName = contactsMap.get(participantJid).name; - } else if (participantJid) { - m.pushName = participantJid.split('@')[0]; - } - } - messagesRaw.push(this.prepareMessage(m)); } - this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw], true, undefined, { - isLatest, - progress, - }); + this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { - await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true }); + await this.prismaRepository.message.createMany({ + data: messagesRaw, + skipDuplicates: true, + }); } if ( @@ -1068,7 +1046,12 @@ export class BaileysStartupService extends ChannelStartupService { } await this.contactHandle['contacts.upsert']( - contacts.filter((c) => !!c.notify || !!c.name).map((c) => ({ id: c.id, name: c.name ?? c.notify })), + contacts + .filter((c) => !!c.notify || !!c.name) + .map((c) => ({ + id: c.id, + name: c.name ?? c.notify, + })), ); contacts = undefined; @@ -1080,28 +1063,19 @@ export class BaileysStartupService extends ChannelStartupService { }, 'messages.upsert': async ( - { messages, type, requestId }: { messages: WAMessage[]; type: MessageUpsertType; requestId?: string }, + { + messages, + type, + requestId, + }: { + messages: proto.IWebMessageInfo[]; + type: MessageUpsertType; + requestId?: string; + }, settings: any, ) => { try { for (const received of messages) { - if ( - received?.messageStubParameters?.some?.((param) => - [ - 'No matching sessions found for message', - 'Bad MAC', - 'failed to decrypt message', - 'SessionError', - 'Invalid PreKey ID', - 'No session record', - 'No session found to decrypt message', - 'Message absent from node', - ].some((err) => param?.includes?.(err)), - ) - ) { - this.logger.warn(`Message ignored with messageStubParameters: ${JSON.stringify(received, null, 2)}`); - continue; - } if (received.message?.conversation || received.message?.extendedTextMessage?.text) { const text = received.message?.conversation || received.message?.extendedTextMessage?.text; @@ -1119,51 +1093,45 @@ export class BaileysStartupService extends ChannelStartupService { } } - const editedMessage = - received?.message?.protocolMessage || received?.message?.editedMessage?.message?.protocolMessage; + if (received.message?.protocolMessage?.editedMessage || received.message?.editedMessage?.message) { + const editedMessage = + received.message?.protocolMessage || received.message?.editedMessage?.message?.protocolMessage; + if (editedMessage) { + if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) + this.chatwootService.eventWhatsapp( + 'messages.edit', + { instanceName: this.instance.name, instanceId: this.instance.id }, + editedMessage, + ); - if (editedMessage) { - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) - this.chatwootService.eventWhatsapp( - 'messages.edit', - { instanceName: this.instance.name, instanceId: this.instance.id }, - editedMessage, - ); + await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage); + } + } - await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage); + if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') { + this.logger.info(`Recovering message lost messageId: ${received.key.id}`); - if (received.key?.id && editedMessage.key?.id) { - await this.baileysCache.set(`protocol_${received.key.id}`, editedMessage.key.id, 60 * 60 * 24); - } + await this.baileysCache.set(received.key.id, { + message: received, + retry: 0, + }); - const oldMessage = await this.getMessage(editedMessage.key, true); - if ((oldMessage as any)?.id) { - const editedMessageTimestamp = Long.isLong(received?.messageTimestamp) - ? Math.floor(received?.messageTimestamp.toNumber()) - : Math.floor(received?.messageTimestamp as number); + continue; + } - await this.prismaRepository.message.update({ - where: { id: (oldMessage as any).id }, - data: { - message: editedMessage.editedMessage as any, - messageTimestamp: editedMessageTimestamp, - status: 'EDITED', - }, - }); - await this.prismaRepository.messageUpdate.create({ - data: { - fromMe: editedMessage.key.fromMe, - keyId: editedMessage.key.id, - remoteJid: editedMessage.key.remoteJid, - status: 'EDITED', - instanceId: this.instanceId, - messageId: (oldMessage as any).id, - }, - }); - } + const retryCache = (await this.baileysCache.get(received.key.id)) || null; + + if (retryCache) { + this.logger.info('Recovered message lost'); + await this.baileysCache.delete(received.key.id); } - if ((type !== 'notify' && type !== 'append') || editedMessage || !received?.message) { + if ( + (type !== 'notify' && type !== 'append') || + received.message?.protocolMessage || + received.message?.pollUpdateMessage || + !received?.message + ) { continue; } @@ -1171,10 +1139,14 @@ export class BaileysStartupService extends ChannelStartupService { received.messageTimestamp = received.messageTimestamp?.toNumber(); } + // Resolve @lid to @s.whatsapp.net so all downstream uses (prepareMessage, chatbot, contact) get the correct JID + if (received.key.remoteJid?.includes('@lid') && (received.key as LidMessageKey)?.remoteJidAlt) { + received.key.remoteJid = (received.key as LidMessageKey).remoteJidAlt; + } + if (settings?.groupsIgnore && received.key.remoteJid.includes('@g.us')) { continue; } - const existingChat = await this.prismaRepository.chat.findFirst({ where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid }, select: { id: true, name: true }, @@ -1203,107 +1175,6 @@ export class BaileysStartupService extends ChannelStartupService { const messageRaw = this.prepareMessage(received); - if (messageRaw.messageType === 'pollUpdateMessage') { - const pollCreationKey = messageRaw.message.pollUpdateMessage.pollCreationMessageKey; - const pollMessage = (await this.getMessage(pollCreationKey, true)) as proto.IWebMessageInfo; - const pollMessageSecret = (await this.getMessage(pollCreationKey)) as any; - - if (pollMessage) { - const pollOptions = - (pollMessage.message as any).pollCreationMessage?.options || - (pollMessage.message as any).pollCreationMessageV3?.options || - []; - const pollVote = messageRaw.message.pollUpdateMessage.vote; - - const voterJid = received.key.fromMe - ? this.instance.wuid - : received.key.participant || received.key.remoteJid; - - let pollEncKey = pollMessageSecret?.messageContextInfo?.messageSecret; - - let successfulVoterJid = voterJid; - - if (typeof pollEncKey === 'string') { - pollEncKey = Buffer.from(pollEncKey, 'base64'); - } else if (pollEncKey?.type === 'Buffer' && Array.isArray(pollEncKey.data)) { - pollEncKey = Buffer.from(pollEncKey.data); - } - - if (Buffer.isBuffer(pollEncKey) && pollEncKey.length === 44) { - pollEncKey = Buffer.from(pollEncKey.toString('utf8'), 'base64'); - } - - if (pollVote.encPayload && pollEncKey) { - const creatorCandidates = [ - this.instance.wuid, - this.client.user?.lid, - pollMessage.key.participant, - (pollMessage.key as any).participantAlt, - pollMessage.key.remoteJid, - ]; - - const key = received.key as any; - const voterCandidates = [ - this.instance.wuid, - this.client.user?.lid, - key.participant, - key.participantAlt, - key.remoteJidAlt, - key.remoteJid, - ]; - - const uniqueCreators = [ - ...new Set(creatorCandidates.filter(Boolean).map((id) => jidNormalizedUser(id))), - ]; - const uniqueVoters = [...new Set(voterCandidates.filter(Boolean).map((id) => jidNormalizedUser(id)))]; - - let decryptedVote; - - for (const creator of uniqueCreators) { - for (const voter of uniqueVoters) { - try { - decryptedVote = decryptPollVote(pollVote, { - pollCreatorJid: creator, - pollMsgId: pollMessage.key.id, - pollEncKey, - voterJid: voter, - } as any); - if (decryptedVote) { - successfulVoterJid = voter; - break; - } - } catch { - // Continue trying - } - } - if (decryptedVote) break; - } - - if (decryptedVote) { - Object.assign(pollVote, decryptedVote); - } - } - - const selectedOptions = pollVote?.selectedOptions || []; - - const selectedOptionNames = pollOptions - .filter((option) => { - const hash = createHash('sha256').update(option.optionName).digest(); - return selectedOptions.some((selected) => Buffer.compare(selected, hash) === 0); - }) - .map((option) => option.optionName); - - messageRaw.message.pollUpdateMessage.vote.selectedOptions = selectedOptionNames; - - const pollUpdates = pollOptions.map((option) => ({ - name: option.optionName, - voters: selectedOptionNames.includes(option.optionName) ? [successfulVoterJid] : [], - })); - - messageRaw.pollUpdates = pollUpdates; - } - } - const isMedia = received?.message?.imageMessage || received?.message?.videoMessage || @@ -1313,8 +1184,6 @@ export class BaileysStartupService extends ChannelStartupService { received?.message?.ptvMessage || received?.message?.audioMessage; - const isVideo = received?.message?.videoMessage; - if (this.localSettings.readMessages && received.key.id !== 'status@broadcast') { await this.client.readMessages([received.key]); } @@ -1330,7 +1199,7 @@ export class BaileysStartupService extends ChannelStartupService { ) { const chatwootSentMessage = await this.chatwootService.eventWhatsapp( Events.MESSAGES_UPSERT, - { instanceName: this.instance.name, instanceId: this.instanceId }, + { instanceName: this.instance.name, instanceId: this.instance.id }, messageRaw, ); @@ -1343,97 +1212,81 @@ export class BaileysStartupService extends ChannelStartupService { if (this.configService.get('OPENAI').ENABLED && received?.message?.audioMessage) { const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ - where: { instanceId: this.instanceId }, - include: { OpenaiCreds: true }, + where: { + instanceId: this.instanceId, + }, + include: { + OpenaiCreds: true, + }, }); if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { - messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`; + messageRaw.message.speechToText = await this.openaiService.speechToText( + openAiDefaultSettings.OpenaiCreds, + received, + this.client.updateMediaMessage, + ); } } if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { pollUpdates, ...messageData } = messageRaw; - const msg = await this.prismaRepository.message.create({ data: messageData }); - - const { remoteJid } = received.key; - const timestamp = msg.messageTimestamp; - const fromMe = received.key.fromMe.toString(); - const messageKey = `${remoteJid}_${timestamp}_${fromMe}`; - - const cachedTimestamp = await this.baileysCache.get(messageKey); - - if (!cachedTimestamp) { - if (!received.key.fromMe) { - if (msg.status === status[3]) { - this.logger.log(`Update not read messages ${remoteJid}`); - await this.updateChatUnreadMessages(remoteJid); - } else if (msg.status === status[4]) { - this.logger.log(`Update readed messages ${remoteJid} - ${timestamp}`); - await this.updateMessagesReadedByTimestamp(remoteJid, timestamp); - } - } else { - // is send message by me - this.logger.log(`Update readed messages ${remoteJid} - ${timestamp}`); - await this.updateMessagesReadedByTimestamp(remoteJid, timestamp); - } + const msg = await this.prismaRepository.message.create({ + data: messageRaw, + }); + + if (received.key.fromMe === false) { + if (msg.status === status[3]) { + this.logger.log(`Update not read messages ${received.key.remoteJid}`); + + await this.updateChatUnreadMessages(received.key.remoteJid); + } else if (msg.status === status[4]) { + this.logger.log(`Update readed messages ${received.key.remoteJid} - ${msg.messageTimestamp}`); - await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS); + await this.updateMessagesReadedByTimestamp(received.key.remoteJid, msg.messageTimestamp); + } } else { - this.logger.info(`Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}`); + // is send message by me + this.logger.log(`Update readed messages ${received.key.remoteJid} - ${msg.messageTimestamp}`); + + await this.updateMessagesReadedByTimestamp(received.key.remoteJid, msg.messageTimestamp); } if (isMedia) { if (this.configService.get('S3').ENABLE) { try { - if (isVideo && !this.configService.get('S3').SAVE_VIDEO) { - this.logger.warn('Video upload is disabled. Skipping video upload.'); - // Skip video upload by returning early from this block - return; - } - const message: any = received; + const media = await this.getBase64FromMediaMessage( + { + message, + }, + true, + ); - // Verificação adicional para garantir que há conteúdo de mídia real - const hasRealMedia = this.hasValidMediaContent(message); - - if (!hasRealMedia) { - this.logger.warn('Message detected as media but contains no valid media content'); - } else { - const media = await this.getBase64FromMediaMessage({ message }, true); - - if (!media) { - this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO'); - return; - } - - const { buffer, mediaType, fileName, size } = media; - const mimetype = mimeTypes.lookup(fileName).toString(); - const fullName = join( - `${this.instance.id}`, - received.key.remoteJid, - mediaType, - `${Date.now()}_${fileName}`, - ); - await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype }); - - await this.prismaRepository.media.create({ - data: { - messageId: msg.id, - instanceId: this.instanceId, - type: mediaType, - fileName: fullName, - mimetype, - }, - }); - - const mediaUrl = await s3Service.getObjectUrl(fullName); - - messageRaw.message.mediaUrl = mediaUrl; - - await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); - } + const { buffer, mediaType, fileName, size } = media; + const mimetype = mime.getType(fileName).toString(); + const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName); + await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { + 'Content-Type': mimetype, + }); + + await this.prismaRepository.media.create({ + data: { + messageId: msg.id, + instanceId: this.instanceId, + type: mediaType, + fileName: fullName, + mimetype, + }, + }); + + const mediaUrl = await s3Service.getObjectUrl(fullName); + + messageRaw.message.mediaUrl = mediaUrl; + + await this.prismaRepository.message.update({ + where: { id: msg.id }, + data: messageRaw, + }); } catch (error) { this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); } @@ -1443,42 +1296,21 @@ export class BaileysStartupService extends ChannelStartupService { if (this.localWebhook.enabled) { if (isMedia && this.localWebhook.webhookBase64) { - try { - const buffer = await downloadMediaMessage( - { key: received.key, message: received?.message }, - 'buffer', - {}, - { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, - ); - - if (buffer) { - messageRaw.message.base64 = buffer.toString('base64'); - } else { - // retry to download media - const buffer = await downloadMediaMessage( - { key: received.key, message: received?.message }, - 'buffer', - {}, - { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, - ); + const buffer = await downloadMediaMessage( + { key: received.key, message: received?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + }, + ); - if (buffer) { - messageRaw.message.base64 = buffer.toString('base64'); - } - } - } catch (error) { - this.logger.error(['Error converting media to base64', error?.message]); - } + messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined; } } - this.logger.verbose(messageRaw); - - sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); - if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) { - messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt; - } - console.log(messageRaw); + this.logger.log(messageRaw); this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); @@ -1493,12 +1325,7 @@ export class BaileysStartupService extends ChannelStartupService { where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId }, }); - const contactRaw: { - remoteJid: string; - pushName: string; - profilePicUrl?: string; - instanceId: string; - } = { + const contactRaw: { remoteJid: string; pushName: string; profilePicUrl?: string; instanceId: string } = { remoteJid: received.key.remoteJid, pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName, profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, @@ -1509,17 +1336,6 @@ export class BaileysStartupService extends ChannelStartupService { continue; } - if (contactRaw.remoteJid.includes('@s.whatsapp') || contactRaw.remoteJid.includes('@lid')) { - await saveOnWhatsappCache([ - { - remoteJid: - messageRaw.key.addressingMode === 'lid' ? messageRaw.key.remoteJidAlt : messageRaw.key.remoteJid, - remoteJidAlt: messageRaw.key.remoteJidAlt, - lid: messageRaw.key.addressingMode === 'lid' ? 'lid' : null, - }, - ]); - } - if (contact) { this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); @@ -1545,18 +1361,27 @@ export class BaileysStartupService extends ChannelStartupService { if (this.configService.get('DATABASE').SAVE_DATA.CONTACTS) await this.prismaRepository.contact.upsert({ - where: { remoteJid_instanceId: { remoteJid: contactRaw.remoteJid, instanceId: contactRaw.instanceId } }, + where: { + remoteJid_instanceId: { + remoteJid: contactRaw.remoteJid, + instanceId: contactRaw.instanceId, + }, + }, update: contactRaw, create: contactRaw, }); + + if (contactRaw.remoteJid.includes('@s.whatsapp')) { + await saveOnWhatsappCache([{ remoteJid: contactRaw.remoteJid }]); + } } } catch (error) { this.logger.error(error); } }, - 'messages.update': async (args: { update: Partial; key: WAMessageKey }[], settings: any) => { - this.logger.verbose(`Update messages ${JSON.stringify(args, undefined, 2)}`); + 'messages.update': async (args: WAMessageUpdate[], settings: any) => { + this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`); const readChatToUpdate: Record = {}; // {remoteJid: true} @@ -1565,27 +1390,6 @@ export class BaileysStartupService extends ChannelStartupService { continue; } - const updateKey = `${this.instance.id}_${key.id}_${update.status}`; - - const cached = await this.baileysCache.get(updateKey); - - const secondsSinceEpoch = Math.floor(Date.now() / 1000); - console.log('CACHE:', { cached, updateKey, messageTimestamp: update.messageTimestamp, secondsSinceEpoch }); - - if ( - (update.messageTimestamp && update.messageTimestamp === cached) || - (!update.messageTimestamp && secondsSinceEpoch === cached) - ) { - this.logger.info(`Update Message duplicated ignored [avoid deadlock]: ${updateKey}`); - continue; - } - - if (update.messageTimestamp) { - await this.baileysCache.set(updateKey, update.messageTimestamp, 30 * 60); - } else { - await this.baileysCache.set(updateKey, secondsSinceEpoch, 30 * 60); - } - if (status[update.status] === 'READ' && key.fromMe) { if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { this.chatwootService.eventWhatsapp( @@ -1596,7 +1400,7 @@ export class BaileysStartupService extends ChannelStartupService { } } - if (key.remoteJid !== 'status@broadcast' && key.id !== undefined) { + if (key.remoteJid !== 'status@broadcast') { let pollUpdates: any; if (update.pollUpdates) { @@ -1610,53 +1414,43 @@ export class BaileysStartupService extends ChannelStartupService { } } - const message: any = { - keyId: key.id, - remoteJid: key?.remoteJid, - fromMe: key.fromMe, - participant: key?.participant, - status: status[update.status] ?? 'SERVER_ACK', - pollUpdates, - instanceId: this.instanceId, - }; + const findMessage = await this.prismaRepository.message.findFirst({ + where: { + instanceId: this.instanceId, + key: { + path: ['id'], + equals: key.id, + }, + }, + }); - if (update.message) { - message.message = update.message; + if (!findMessage) { + continue; } - let findMessage: any; - const configDatabaseData = this.configService.get('DATABASE').SAVE_DATA; - if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) { - // Use raw SQL to avoid JSON path issues - const protocolMapKey = `protocol_${key.id}`; - const originalMessageId = (await this.baileysCache.get(protocolMapKey)) as string; - - if (originalMessageId) { - message.keyId = originalMessageId; - } - - const searchId = originalMessageId || key.id; - - const messages = (await this.prismaRepository.$queryRaw` - SELECT * FROM "Message" - WHERE "instanceId" = ${this.instanceId} - AND "key"->>'id' = ${searchId} - LIMIT 1 - `) as any[]; - findMessage = messages[0] || null; - - if (!findMessage?.id) { - this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`); - continue; - } - message.messageId = findMessage.id; - } + // Resolve @lid to @s.whatsapp.net using the stored message key as source of truth + const lidKey = key as LidMessageKey; + const resolvedRemoteJid: string = key.remoteJid?.includes('@lid') + ? (lidKey.remoteJidAlt ?? (findMessage.key as LidMessageKey)?.remoteJid ?? key.remoteJid) + : key.remoteJid; if (update.message === null && update.status === undefined) { - this.sendDataWebhook(Events.MESSAGES_DELETE, { ...key, status: 'DELETED' }); + this.sendDataWebhook(Events.MESSAGES_DELETE, key); + + const message: any = { + messageId: findMessage.id, + keyId: key.id, + remoteJid: resolvedRemoteJid, + fromMe: key.fromMe, + participant: resolvedRemoteJid, + status: 'DELETED', + instanceId: this.instanceId, + }; if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) - await this.prismaRepository.messageUpdate.create({ data: message }); + await this.prismaRepository.messageUpdate.create({ + data: message, + }); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { this.chatwootService.eventWhatsapp( @@ -1667,60 +1461,57 @@ export class BaileysStartupService extends ChannelStartupService { } continue; - } - - if (findMessage && update.status !== undefined && status[update.status] !== findMessage.status) { - if (!key.fromMe && key.remoteJid) { - readChatToUpdate[key.remoteJid] = true; - - const { remoteJid } = key; - const timestamp = findMessage.messageTimestamp; - const fromMe = key.fromMe.toString(); - const messageKey = `${remoteJid}_${timestamp}_${fromMe}`; + } else if (update.status !== undefined && status[update.status] !== findMessage.status) { + if (!key.fromMe && resolvedRemoteJid) { + readChatToUpdate[resolvedRemoteJid] = true; - const cachedTimestamp = await this.baileysCache.get(messageKey); - - if (!cachedTimestamp) { - if (status[update.status] === status[4]) { - this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`); - await this.updateMessagesReadedByTimestamp(remoteJid, timestamp); - await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS); - } - - await this.prismaRepository.message.update({ - where: { id: findMessage.id }, - data: { status: status[update.status] }, - }); - } else { - this.logger.info( - `Update readed messages duplicated ignored in message.update [avoid deadlock]: ${messageKey}`, - ); + if (status[update.status] === status[4]) { + this.logger.log(`Update as read ${resolvedRemoteJid} - ${findMessage.messageTimestamp}`); + this.updateMessagesReadedByTimestamp(resolvedRemoteJid, findMessage.messageTimestamp); } } + + await this.prismaRepository.message.update({ + where: { id: findMessage.id }, + data: { status: status[update.status] }, + }); } + const message: any = { + messageId: findMessage.id, + keyId: key.id, + remoteJid: resolvedRemoteJid, + fromMe: key.fromMe, + participant: resolvedRemoteJid, + status: status[update.status], + pollUpdates, + instanceId: this.instanceId, + }; + this.sendDataWebhook(Events.MESSAGES_UPDATE, message); - if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { message: _msg, ...messageData } = message; - await this.prismaRepository.messageUpdate.create({ data: messageData }); - } + if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) + await this.prismaRepository.messageUpdate.create({ + data: message, + }); const existingChat = await this.prismaRepository.chat.findFirst({ where: { instanceId: this.instanceId, remoteJid: message.remoteJid }, }); if (existingChat) { - const chatToInsert = { remoteJid: message.remoteJid, instanceId: this.instanceId, unreadMessages: 0 }; + const chatToInsert = { + remoteJid: message.remoteJid, + instanceId: this.instanceId, + name: message.pushName || '', + unreadMessages: 0, + }; this.sendDataWebhook(Events.CHATS_UPSERT, [chatToInsert]); if (this.configService.get('DATABASE').SAVE_DATA.CHATS) { - try { - await this.prismaRepository.chat.update({ where: { id: existingChat.id }, data: chatToInsert }); - } catch { - console.log(`Chat insert record ignored: ${chatToInsert.remoteJid} - ${chatToInsert.instanceId}`); - } + await this.prismaRepository.chat.create({ + data: chatToInsert, + }); } } } @@ -1745,66 +1536,12 @@ export class BaileysStartupService extends ChannelStartupService { }); }, - 'group-participants.update': async (participantsUpdate: { + 'group-participants.update': (participantsUpdate: { id: string; participants: string[]; action: ParticipantAction; }) => { - // ENHANCEMENT: Adds participantsData field while maintaining backward compatibility - // MAINTAINS: participants: string[] (original JID strings) - // ADDS: participantsData: { jid: string, phoneNumber: string, name?: string, imgUrl?: string }[] - // This enables LID to phoneNumber conversion without breaking existing webhook consumers - - // Helper to normalize participantId as phone number - const normalizePhoneNumber = (id: string | null | undefined): string => { - // Remove @lid, @s.whatsapp.net suffixes and extract just the number part - return String(id || '').split('@')[0]; - }; - - try { - // Usa o mesmo método que o endpoint /group/participants - const groupParticipants = await this.findParticipants({ groupJid: participantsUpdate.id }); - - // Validação para garantir que temos dados válidos - if (!groupParticipants?.participants || !Array.isArray(groupParticipants.participants)) { - throw new Error('Invalid participant data received from findParticipants'); - } - - // Filtra apenas os participantes que estão no evento - const resolvedParticipants = participantsUpdate.participants.map((participantId) => { - const participantData = groupParticipants.participants.find((p) => p.id === participantId); - - let phoneNumber: string; - if (participantData?.phoneNumber) { - phoneNumber = participantData.phoneNumber; - } else { - phoneNumber = normalizePhoneNumber(participantId); - } - - return { - jid: participantId, - phoneNumber, - name: participantData?.name, - imgUrl: participantData?.imgUrl, - }; - }); - - // Mantém formato original + adiciona dados resolvidos - const enhancedParticipantsUpdate = { - ...participantsUpdate, - participants: participantsUpdate.participants, // Mantém array original de strings - // Adiciona dados resolvidos em campo separado - participantsData: resolvedParticipants, - }; - - this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, enhancedParticipantsUpdate); - } catch (error) { - this.logger.error( - `Failed to resolve participant data for GROUP_PARTICIPANTS_UPDATE webhook: ${error.message} | Group: ${participantsUpdate.id} | Participants: ${participantsUpdate.participants.length}`, - ); - // Fallback - envia sem conversão - this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); - } + this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); this.updateGroupMetadataCache(participantsUpdate.id); }, @@ -1812,9 +1549,9 @@ export class BaileysStartupService extends ChannelStartupService { private readonly labelHandle = { [Events.LABELS_EDIT]: async (label: Label) => { - this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name }); - - const labelsRepository = await this.prismaRepository.label.findMany({ where: { instanceId: this.instanceId } }); + const labelsRepository = await this.prismaRepository.label.findMany({ + where: { instanceId: this.instanceId }, + }); const savedLabel = labelsRepository.find((l) => l.labelId === label.id); if (label.deleted && savedLabel) { @@ -1836,11 +1573,17 @@ export class BaileysStartupService extends ChannelStartupService { instanceId: this.instanceId, }; await this.prismaRepository.label.upsert({ - where: { labelId_instanceId: { instanceId: labelData.instanceId, labelId: labelData.labelId } }, + where: { + labelId_instanceId: { + instanceId: labelData.instanceId, + labelId: labelData.labelId, + }, + }, update: labelData, create: labelData, }); } + this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name }); } }, @@ -1848,18 +1591,26 @@ export class BaileysStartupService extends ChannelStartupService { data: { association: LabelAssociation; type: 'remove' | 'add' }, database: Database, ) => { - this.logger.info( - `labels association - ${data?.association?.chatId} (${data.type}-${data?.association?.type}): ${data?.association?.labelId}`, - ); if (database.SAVE_DATA.CHATS) { - const instanceId = this.instanceId; - const chatId = data.association.chatId; - const labelId = data.association.labelId; - - if (data.type === 'add') { - await this.addLabel(labelId, instanceId, chatId); - } else if (data.type === 'remove') { - await this.removeLabel(labelId, instanceId, chatId); + const chats = await this.prismaRepository.chat.findMany({ + where: { instanceId: this.instanceId }, + }); + const chat = chats.find((c) => c.remoteJid === data.association.chatId); + if (chat) { + const labelsArray = Array.isArray(chat.labels) ? chat.labels.map((event) => String(event)) : []; + let labels = [...labelsArray]; + + if (data.type === 'remove') { + labels = labels.filter((label) => label !== data.association.labelId); + } else if (data.type === 'add') { + labels = [...labels, data.association.labelId]; + } + await this.prismaRepository.chat.update({ + where: { id: chat.id }, + data: { + labels, + }, + }); } } @@ -1874,141 +1625,137 @@ export class BaileysStartupService extends ChannelStartupService { private eventHandler() { this.client.ev.process(async (events) => { - this.eventProcessingQueue = this.eventProcessingQueue.then(async () => { - try { - if (!this.endSession) { - const database = this.configService.get('DATABASE'); - const settings = await this.findSettings(); - - if (events.call) { - const call = events.call[0]; - - if (settings?.rejectCall && call.status == 'offer') { - this.client.rejectCall(call.id, call.from); - } + if (!this.endSession) { + const database = this.configService.get('DATABASE'); + const settings = await this.findSettings(); - if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') { - if (call.from.endsWith('@lid')) { - call.from = await this.client.signalRepository.lidMapping.getPNForLID(call.from as string); - } - const msg = await this.client.sendMessage(call.from, { text: settings.msgCall }); + if (events.call) { + const call = events.call[0]; - this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify' }); - } + if (settings?.rejectCall && call.status == 'offer') { + this.client.rejectCall(call.id, call.from); + } - this.sendDataWebhook(Events.CALL, call); - } + if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') { + const msg = await this.client.sendMessage(call.from, { + text: settings.msgCall, + }); - if (events['connection.update']) { - this.connectionUpdate(events['connection.update']); - } + this.client.ev.emit('messages.upsert', { + messages: [msg], + type: 'notify', + }); + } - if (events['creds.update']) { - this.instance.authState.saveCreds(); - } + this.sendDataWebhook(Events.CALL, call); + } - if (events['messaging-history.set']) { - const payload = events['messaging-history.set']; - await this.messageHandle['messaging-history.set'](payload); - } + if (events['connection.update']) { + this.connectionUpdate(events['connection.update']); + } - if (events['messages.upsert']) { - const payload = events['messages.upsert']; + if (events['creds.update']) { + this.instance.authState.saveCreds(); + } - // this.messageProcessor.processMessage(payload, settings); - await this.messageHandle['messages.upsert'](payload, settings); - } + if (events['messaging-history.set']) { + const payload = events['messaging-history.set']; + this.messageHandle['messaging-history.set'](payload); + } - if (events['messages.update']) { - const payload = events['messages.update']; - await this.messageHandle['messages.update'](payload, settings); - } + if (events['messages.upsert']) { + const payload = events['messages.upsert']; + this.messageHandle['messages.upsert'](payload, settings); + } - if (events['message-receipt.update']) { - const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[]; - const remotesJidMap: Record = {}; + if (events['messages.update']) { + const payload = events['messages.update']; + this.messageHandle['messages.update'](payload, settings); + } - for (const event of payload) { - if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') { - remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp; - } - } + if (events['message-receipt.update']) { + const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[]; + const remotesJidMap: Record = {}; - await Promise.all( - Object.keys(remotesJidMap).map(async (remoteJid) => - this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]), - ), - ); + for (const event of payload) { + if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') { + // Resolve @lid to @s.whatsapp.net so read receipts match stored messages + const jid = resolveLidJid(event.key as LidMessageKey); + remotesJidMap[jid] = event.receipt.readTimestamp; } + } - if (events['presence.update']) { - const payload = events['presence.update']; + await Promise.all( + Object.keys(remotesJidMap).map(async (remoteJid) => + this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]), + ), + ); + } - if (settings?.groupsIgnore && payload.id.includes('@g.us')) { - return; - } + if (events['presence.update']) { + const payload = events['presence.update']; - this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); - } + if (settings?.groupsIgnore && payload.id.includes('@g.us')) { + return; + } - if (!settings?.groupsIgnore) { - if (events['groups.upsert']) { - const payload = events['groups.upsert']; - this.groupHandler['groups.upsert'](payload); - } + this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); + } - if (events['groups.update']) { - const payload = events['groups.update']; - this.groupHandler['groups.update'](payload); - } + if (!settings?.groupsIgnore) { + if (events['groups.upsert']) { + const payload = events['groups.upsert']; + this.groupHandler['groups.upsert'](payload); + } - if (events['group-participants.update']) { - const payload = events['group-participants.update'] as any; - this.groupHandler['group-participants.update'](payload); - } - } + if (events['groups.update']) { + const payload = events['groups.update']; + this.groupHandler['groups.update'](payload); + } - if (events['chats.upsert']) { - const payload = events['chats.upsert']; - this.chatHandle['chats.upsert'](payload); - } + if (events['group-participants.update']) { + const payload = events['group-participants.update']; + this.groupHandler['group-participants.update'](payload); + } + } - if (events['chats.update']) { - const payload = events['chats.update']; - this.chatHandle['chats.update'](payload); - } + if (events['chats.upsert']) { + const payload = events['chats.upsert']; + this.chatHandle['chats.upsert'](payload); + } - if (events['chats.delete']) { - const payload = events['chats.delete']; - this.chatHandle['chats.delete'](payload); - } + if (events['chats.update']) { + const payload = events['chats.update']; + this.chatHandle['chats.update'](payload); + } - if (events['contacts.upsert']) { - const payload = events['contacts.upsert']; - this.contactHandle['contacts.upsert'](payload); - } + if (events['chats.delete']) { + const payload = events['chats.delete']; + this.chatHandle['chats.delete'](payload); + } - if (events['contacts.update']) { - const payload = events['contacts.update']; - this.contactHandle['contacts.update'](payload); - } + if (events['contacts.upsert']) { + const payload = events['contacts.upsert']; + this.contactHandle['contacts.upsert'](payload); + } - if (events[Events.LABELS_ASSOCIATION]) { - const payload = events[Events.LABELS_ASSOCIATION]; - this.labelHandle[Events.LABELS_ASSOCIATION](payload, database); - return; - } + if (events['contacts.update']) { + const payload = events['contacts.update']; + this.contactHandle['contacts.update'](payload); + } - if (events[Events.LABELS_EDIT]) { - const payload = events[Events.LABELS_EDIT]; - this.labelHandle[Events.LABELS_EDIT](payload); - return; - } - } - } catch (error) { - this.logger.error(error); + if (events[Events.LABELS_ASSOCIATION]) { + const payload = events[Events.LABELS_ASSOCIATION]; + this.labelHandle[Events.LABELS_ASSOCIATION](payload, database); + return; } - }); + + if (events[Events.LABELS_EDIT]) { + const payload = events[Events.LABELS_EDIT]; + this.labelHandle[Events.LABELS_EDIT](payload); + return; + } + } }); } @@ -2048,9 +1795,15 @@ export class BaileysStartupService extends ChannelStartupService { try { const profilePictureUrl = await this.client.profilePictureUrl(jid, 'image'); - return { wuid: jid, profilePictureUrl }; - } catch { - return { wuid: jid, profilePictureUrl: null }; + return { + wuid: jid, + profilePictureUrl, + }; + } catch (error) { + return { + wuid: jid, + profilePictureUrl: null, + }; } } @@ -2058,9 +1811,15 @@ export class BaileysStartupService extends ChannelStartupService { const jid = createJid(number); try { - return { wuid: jid, status: (await this.client.fetchStatus(jid))[0]?.status }; - } catch { - return { wuid: jid, status: null }; + return { + wuid: jid, + status: (await this.client.fetchStatus(jid))?.status, + }; + } catch (error) { + return { + wuid: jid, + status: null, + }; } } @@ -2108,8 +1867,15 @@ export class BaileysStartupService extends ChannelStartupService { website: business?.website?.shift(), }; } - } catch { - return { wuid: jid, name: null, picture: null, status: null, os: null, isBusiness: false }; + } catch (error) { + return { + wuid: jid, + name: null, + picture: null, + status: null, + os: null, + isBusiness: false, + }; } } @@ -2117,11 +1883,10 @@ export class BaileysStartupService extends ChannelStartupService { const jid = createJid(number); try { - // const call = await this.client.offerCall(jid, isVideo); - // setTimeout(() => this.client.terminateCall(call.id, call.to), callDuration * 1000); + const call = await this.client.offerCall(jid, isVideo); + setTimeout(() => this.client.terminateCall(call.id, call.to), callDuration * 1000); - // return call; - return { id: '123', jid, isVideo, callDuration }; + return call; } catch (error) { return error; } @@ -2135,12 +1900,13 @@ export class BaileysStartupService extends ChannelStartupService { quoted: any, messageId?: string, ephemeralExpiration?: number, - contextInfo?: any, // participants?: GroupParticipant[], ) { sender = sender.toLowerCase(); - const option: any = { quoted }; + const option: any = { + quoted, + }; if (isJidGroup(sender)) { option.useCachedGroupMetadata = true; @@ -2152,8 +1918,8 @@ export class BaileysStartupService extends ChannelStartupService { if (ephemeralExpiration) option.ephemeralExpiration = ephemeralExpiration; - // NOTE: NÃO DEVEMOS GERAR O messageId AQUI, SOMENTE SE VIER INFORMADO POR PARAMETRO. A GERAÇÃO ANTERIOR IMPEDE O WZAP DE IDENTIFICAR A SOURCE. if (messageId) option.messageId = messageId; + else option.messageId = '3EB0' + randomBytes(18).toString('hex').toUpperCase(); if (message['viewOnceMessage']) { const m = generateWAMessageFromContent(sender, message, { @@ -2163,7 +1929,12 @@ export class BaileysStartupService extends ChannelStartupService { quoted, }); const id = await this.client.relayMessage(sender, message, { messageId }); - m.key = { id: id, remoteJid: sender, participant: isPnUser(sender) ? sender : undefined, fromMe: true }; + m.key = { + id: id, + remoteJid: sender, + participant: isJidUser(sender) ? sender : undefined, + fromMe: true, + }; for (const [key, value] of Object.entries(m)) { if (!value || (isArray(value) && value.length) === 0) { delete m[key]; @@ -2183,17 +1954,16 @@ export class BaileysStartupService extends ChannelStartupService { return await this.client.sendMessage( sender, { - react: { text: message['reactionMessage']['text'], key: message['reactionMessage']['key'] }, + react: { + text: message['reactionMessage']['text'], + key: message['reactionMessage']['key'], + }, } as unknown as AnyMessageContent, option as unknown as MiscMessageGenerationOptions, ); } } - if (contextInfo) { - message['contextInfo'] = contextInfo; - } - if (message['conversation']) { return await this.client.sendMessage( sender, @@ -2201,7 +1971,6 @@ export class BaileysStartupService extends ChannelStartupService { text: message['conversation'], mentions, linkPreview: linkPreview, - contextInfo: message['contextInfo'], } as unknown as AnyMessageContent, option as unknown as MiscMessageGenerationOptions, ); @@ -2211,9 +1980,11 @@ export class BaileysStartupService extends ChannelStartupService { return await this.client.sendMessage( sender, { - forward: { key: { remoteJid: this.instance.wuid, fromMe: true }, message }, + forward: { + key: { remoteJid: this.instance.wuid, fromMe: true }, + message, + }, mentions, - contextInfo: message['contextInfo'], }, option as unknown as MiscMessageGenerationOptions, ); @@ -2223,7 +1994,14 @@ export class BaileysStartupService extends ChannelStartupService { let jidList; if (message['status'].option.allContacts) { const contacts = await this.prismaRepository.contact.findMany({ - where: { instanceId: this.instanceId, remoteJid: { not: { endsWith: '@g.us' } } }, + where: { + instanceId: this.instanceId, + remoteJid: { + not: { + endsWith: '@g.us', + }, + }, + }, }); jidList = contacts.map((contact) => contact.remoteJid); @@ -2345,7 +2123,7 @@ export class BaileysStartupService extends ChannelStartupService { if (options?.quoted) { const m = options?.quoted; - const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as WAMessage); + const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); if (msg) { quoted = msg; @@ -2355,16 +2133,13 @@ export class BaileysStartupService extends ChannelStartupService { let messageSent: WAMessage; let mentions: string[]; - let contextInfo: any; - if (isJidGroup(sender)) { let group; try { const cache = this.configService.get('CACHE'); if (!cache.REDIS.ENABLED && !cache.LOCAL.ENABLED) group = await this.findGroup({ groupJid: sender }, 'inner'); else group = await this.getGroupMetadataCache(sender); - // group = await this.findGroup({ groupJid: sender }, 'inner'); - } catch { + } catch (error) { throw new NotFoundException('Group not found'); } @@ -2374,7 +2149,7 @@ export class BaileysStartupService extends ChannelStartupService { if (options?.mentionsEveryOne) { mentions = group.participants.map((participant) => participant.id); - } else if (options?.mentioned?.length) { + } else if (options.mentioned?.length) { mentions = options.mentioned.map((mention) => { const jid = createJid(mention); if (isJidGroup(jid)) { @@ -2395,27 +2170,7 @@ export class BaileysStartupService extends ChannelStartupService { // group?.participants, ); } else { - contextInfo = { - mentionedJid: [], - groupMentions: [], - //expiration: 7776000, - ephemeralSettingTimestamp: { - low: Math.floor(Date.now() / 1000) - 172800, - high: 0, - unsigned: false, - }, - disappearingMode: { initiator: 0 }, - }; - messageSent = await this.sendMessage( - sender, - message, - mentions, - linkPreview, - quoted, - null, - undefined, - contextInfo, - ); + messageSent = await this.sendMessage(sender, message, mentions, linkPreview, quoted); } if (Long.isLong(messageSent?.messageTimestamp)) { @@ -2434,8 +2189,6 @@ export class BaileysStartupService extends ChannelStartupService { messageSent?.message?.ptvMessage || messageSent?.message?.audioMessage; - const isVideo = messageSent?.message?.videoMessage; - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !isIntegration) { this.chatwootService.eventWhatsapp( Events.SEND_MESSAGE, @@ -2446,63 +2199,72 @@ export class BaileysStartupService extends ChannelStartupService { if (this.configService.get('OPENAI').ENABLED && messageRaw?.message?.audioMessage) { const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ - where: { instanceId: this.instanceId }, - include: { OpenaiCreds: true }, + where: { + instanceId: this.instanceId, + }, + include: { + OpenaiCreds: true, + }, }); if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { - messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(messageRaw, this)}`; + messageRaw.message.speechToText = await this.openaiService.speechToText( + openAiDefaultSettings.OpenaiCreds, + messageRaw, + this.client.updateMediaMessage, + ); } } if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { - const msg = await this.prismaRepository.message.create({ data: messageRaw }); + const msg = await this.prismaRepository.message.create({ + data: messageRaw, + }); if (isMedia && this.configService.get('S3').ENABLE) { try { - if (isVideo && !this.configService.get('S3').SAVE_VIDEO) { - throw new Error('Video upload is disabled.'); - } - const message: any = messageRaw; + const media = await this.getBase64FromMediaMessage( + { + message, + }, + true, + ); - // Verificação adicional para garantir que há conteúdo de mídia real - const hasRealMedia = this.hasValidMediaContent(message); - - if (!hasRealMedia) { - this.logger.warn('Message detected as media but contains no valid media content'); - } else { - const media = await this.getBase64FromMediaMessage({ message }, true); - - if (!media) { - this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO'); - return; - } - - const { buffer, mediaType, fileName, size } = media; + const { buffer, mediaType, fileName, size } = media; - const mimetype = mimeTypes.lookup(fileName).toString(); + const mimetype = mime.getType(fileName).toString(); - const fullName = join( - `${this.instance.id}`, - messageRaw.key.remoteJid, - `${messageRaw.key.id}`, - mediaType, - fileName, - ); + const fullName = join( + `${this.instance.id}`, + messageRaw.key.remoteJid, + `${messageRaw.key.id}`, + mediaType, + fileName, + ); - await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype }); + await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { + 'Content-Type': mimetype, + }); - await this.prismaRepository.media.create({ - data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype }, - }); + await this.prismaRepository.media.create({ + data: { + messageId: msg.id, + instanceId: this.instanceId, + type: mediaType, + fileName: fullName, + mimetype, + }, + }); - const mediaUrl = await s3Service.getObjectUrl(fullName); + const mediaUrl = await s3Service.getObjectUrl(fullName); - messageRaw.message.mediaUrl = mediaUrl; + messageRaw.message.mediaUrl = mediaUrl; - await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); - } + await this.prismaRepository.message.update({ + where: { id: msg.id }, + data: messageRaw, + }); } catch (error) { this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); } @@ -2511,36 +2273,21 @@ export class BaileysStartupService extends ChannelStartupService { if (this.localWebhook.enabled) { if (isMedia && this.localWebhook.webhookBase64) { - try { - const buffer = await downloadMediaMessage( - { key: messageRaw.key, message: messageRaw?.message }, - 'buffer', - {}, - { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, - ); - - if (buffer) { - messageRaw.message.base64 = buffer.toString('base64'); - } else { - // retry to download media - const buffer = await downloadMediaMessage( - { key: messageRaw.key, message: messageRaw?.message }, - 'buffer', - {}, - { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, - ); + const buffer = await downloadMediaMessage( + { key: messageRaw.key, message: messageRaw?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + }, + ); - if (buffer) { - messageRaw.message.base64 = buffer.toString('base64'); - } - } - } catch (error) { - this.logger.error(['Error converting media to base64', error?.message]); - } + messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined; } } - this.logger.verbose(messageSent); + this.logger.log(messageRaw); this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); @@ -2635,7 +2382,9 @@ export class BaileysStartupService extends ChannelStartupService { return await this.sendMessageWithTyping( data.number, - { conversation: data.text }, + { + conversation: data.text, + }, { delay: data?.delay, presence: 'composing', @@ -2651,7 +2400,13 @@ export class BaileysStartupService extends ChannelStartupService { public async pollMessage(data: SendPollDto) { return await this.sendMessageWithTyping( data.number, - { poll: { name: data.name, selectableCount: data.selectableCount, values: data.values } }, + { + poll: { + name: data.name, + selectableCount: data.selectableCount, + values: data.values, + }, + }, { delay: data?.delay, presence: 'composing', @@ -2673,7 +2428,9 @@ export class BaileysStartupService extends ChannelStartupService { } if (status.allContacts) { - const contacts = await this.prismaRepository.contact.findMany({ where: { instanceId: this.instanceId } }); + const contacts = await this.prismaRepository.contact.findMany({ + where: { instanceId: this.instanceId }, + }); if (!contacts.length) { throw new BadRequestException('Contacts not found'); @@ -2696,21 +2453,41 @@ export class BaileysStartupService extends ChannelStartupService { } return { - content: { text: status.content }, - option: { backgroundColor: status.backgroundColor, font: status.font, statusJidList: status.statusJidList }, + content: { + text: status.content, + }, + option: { + backgroundColor: status.backgroundColor, + font: status.font, + statusJidList: status.statusJidList, + }, }; } if (status.type === 'image') { return { - content: { image: { url: status.content }, caption: status.caption }, - option: { statusJidList: status.statusJidList }, + content: { + image: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, }; } if (status.type === 'video') { return { - content: { video: { url: status.content }, caption: status.caption }, - option: { statusJidList: status.statusJidList }, + content: { + video: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, }; } @@ -2718,8 +2495,14 @@ export class BaileysStartupService extends ChannelStartupService { const convert = await this.processAudioMp4(status.content); if (Buffer.isBuffer(convert)) { const result = { - content: { audio: convert, ptt: true, mimetype: 'audio/ogg; codecs=opus' }, - option: { statusJidList: status.statusJidList }, + content: { + audio: convert, + ptt: true, + mimetype: 'audio/ogg; codecs=opus', + }, + option: { + statusJidList: status.statusJidList, + }, }; return result; @@ -2738,7 +2521,9 @@ export class BaileysStartupService extends ChannelStartupService { const status = await this.formatStatusMessage(mediaData); - const statusSent = await this.sendMessageWithTyping('status@broadcast', { status }); + const statusSent = await this.sendMessageWithTyping('status@broadcast', { + status, + }); return statusSent; } @@ -2747,43 +2532,9 @@ export class BaileysStartupService extends ChannelStartupService { try { const type = mediaMessage.mediatype === 'ptv' ? 'video' : mediaMessage.mediatype; - let mediaInput: any; - if (mediaMessage.mediatype === 'image') { - let imageBuffer: Buffer; - if (isURL(mediaMessage.media)) { - let config: any = { responseType: 'arraybuffer' }; - - if (this.localProxy?.enabled) { - config = { - ...config, - httpsAgent: makeProxyAgent({ - host: this.localProxy.host, - port: this.localProxy.port, - protocol: this.localProxy.protocol, - username: this.localProxy.username, - password: this.localProxy.password, - }), - }; - } - - const response = await axios.get(mediaMessage.media, config); - imageBuffer = Buffer.from(response.data, 'binary'); - } else { - imageBuffer = Buffer.from(mediaMessage.media, 'base64'); - } - - mediaInput = await sharp(imageBuffer).jpeg().toBuffer(); - mediaMessage.fileName ??= 'image.jpg'; - mediaMessage.mimetype = 'image/jpeg'; - } else { - mediaInput = isURL(mediaMessage.media) - ? { url: mediaMessage.media } - : Buffer.from(mediaMessage.media, 'base64'); - } - const prepareMedia = await prepareWAMessageMedia( { - [type]: mediaInput, + [type]: isURL(mediaMessage.media) ? { url: mediaMessage.media } : Buffer.from(mediaMessage.media, 'base64'), } as any, { upload: this.client.waUploadToServer }, ); @@ -2797,22 +2548,24 @@ export class BaileysStartupService extends ChannelStartupService { } if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) { - mediaMessage.fileName = 'image.jpg'; + mediaMessage.fileName = 'image.png'; } if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) { mediaMessage.fileName = 'video.mp4'; } - let mimetype: string | false; + let mimetype: string; if (mediaMessage.mimetype) { mimetype = mediaMessage.mimetype; } else { - mimetype = mimeTypes.lookup(mediaMessage.fileName); + mimetype = mime.getType(mediaMessage.fileName); if (!mimetype && isURL(mediaMessage.media)) { - let config: any = { responseType: 'arraybuffer' }; + let config: any = { + responseType: 'arraybuffer', + }; if (this.localProxy?.enabled) { config = { @@ -2867,18 +2620,14 @@ export class BaileysStartupService extends ChannelStartupService { } } - if (mediaMessage?.fileName) { - mimetype = mimeTypes.lookup(mediaMessage.fileName).toString(); - if (mimetype === 'application/mp4') { - mimetype = 'video/mp4'; - } - } - prepareMedia[mediaType].caption = mediaMessage?.caption; prepareMedia[mediaType].mimetype = mimetype; prepareMedia[mediaType].fileName = mediaMessage.fileName; if (mediaMessage.mediatype === 'video') { + prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( + readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), + ); prepareMedia[mediaType].gifPlayback = false; } @@ -2902,11 +2651,11 @@ export class BaileysStartupService extends ChannelStartupService { imageBuffer = Buffer.from(base64Data, 'base64'); } else { const timestamp = new Date().getTime(); - const parsedURL = new URL(image); - parsedURL.searchParams.set('timestamp', timestamp.toString()); - const url = parsedURL.toString(); + const url = `${image}?timestamp=${timestamp}`; - let config: any = { responseType: 'arraybuffer' }; + let config: any = { + responseType: 'arraybuffer', + }; if (this.localProxy?.enabled) { config = { @@ -2925,47 +2674,28 @@ export class BaileysStartupService extends ChannelStartupService { imageBuffer = Buffer.from(response.data, 'binary'); } - const isAnimated = this.isAnimated(image, imageBuffer); + const webpBuffer = await sharp(imageBuffer).webp().toBuffer(); - if (isAnimated) { - return await sharp(imageBuffer, { animated: true }).webp({ quality: 80 }).toBuffer(); - } else { - return await sharp(imageBuffer).webp().toBuffer(); - } + return webpBuffer; } catch (error) { console.error('Erro ao converter a imagem para WebP:', error); throw error; } } - private isAnimatedWebp(buffer: Buffer): boolean { - if (buffer.length < 12) return false; - - return buffer.indexOf(Buffer.from('ANIM')) !== -1; - } - - private isAnimated(image: string, buffer: Buffer): boolean { - const lowerCaseImage = image.toLowerCase(); - - if (lowerCaseImage.includes('.gif')) return true; - - if (lowerCaseImage.includes('.webp')) return this.isAnimatedWebp(buffer); - - return false; - } - public async mediaSticker(data: SendStickerDto, file?: any) { const mediaData: SendStickerDto = { ...data }; if (file) mediaData.sticker = file.buffer.toString('base64'); - const convert = data?.notConvertSticker - ? Buffer.from(data.sticker, 'base64') - : await this.convertToWebP(data.sticker); + const convert = await this.convertToWebP(data.sticker); const gifPlayback = data.sticker.includes('.gif'); const result = await this.sendMessageWithTyping( data.number, - { sticker: convert, gifPlayback }, + { + sticker: convert, + gifPlayback, + }, { delay: data?.delay, presence: 'composing', @@ -3100,8 +2830,7 @@ export class BaileysStartupService extends ChannelStartupService { } public async processAudio(audio: string): Promise { - const audioConverterConfig = this.configService.get('AUDIO_CONVERTER'); - if (audioConverterConfig.API_URL) { + if (process.env.API_AUDIO_CONVERTER) { this.logger.verbose('Using audio converter API'); const formData = new FormData(); @@ -3111,8 +2840,11 @@ export class BaileysStartupService extends ChannelStartupService { formData.append('base64', audio); } - const { data } = await axios.post(audioConverterConfig.API_URL, formData, { - headers: { ...formData.getHeaders(), apikey: audioConverterConfig.API_KEY }, + const { data } = await axios.post(process.env.API_AUDIO_CONVERTER, formData, { + headers: { + ...formData.getHeaders(), + apikey: process.env.API_AUDIO_CONVERTER_KEY, + }, }); if (!data.audio) { @@ -3126,11 +2858,11 @@ export class BaileysStartupService extends ChannelStartupService { if (isURL(audio)) { const timestamp = new Date().getTime(); - const parsedURL = new URL(audio); - parsedURL.searchParams.set('timestamp', timestamp.toString()); - const url = parsedURL.toString(); + const url = `${audio}?timestamp=${timestamp}`; - const config: any = { responseType: 'stream' }; + const config: any = { + responseType: 'stream', + }; const response = await axios.get(url, config); inputAudioStream = response.data.pipe(new PassThrough()); @@ -3140,8 +2872,6 @@ export class BaileysStartupService extends ChannelStartupService { inputAudioStream.end(audioBuffer); } - const isLpcm = isURL(audio) && /\.lpcm($|\?)/i.test(audio); - return new Promise((resolve, reject) => { const outputAudioStream = new PassThrough(); const chunks: Buffer[] = []; @@ -3159,41 +2889,12 @@ export class BaileysStartupService extends ChannelStartupService { ffmpeg.setFfmpegPath(ffmpegPath.path); - let command = ffmpeg(inputAudioStream); - - if (isLpcm) { - this.logger.verbose('Detected LPCM input – applying raw PCM settings'); - command = command.inputFormat('s16le').inputOptions(['-ar', '24000', '-ac', '1']); - } - - command + ffmpeg(inputAudioStream) .outputFormat('ogg') .noVideo() .audioCodec('libopus') .addOutputOptions('-avoid_negative_ts make_zero') - .audioBitrate('128k') - .audioFrequency(48000) .audioChannels(1) - .outputOptions([ - '-write_xing', - '0', - '-compression_level', - '10', - '-application', - 'voip', - '-fflags', - '+bitexact', - '-flags', - '+bitexact', - '-id3v2_version', - '0', - '-map_metadata', - '-1', - '-map_chapters', - '-1', - '-write_bext', - '0', - ]) .pipe(outputAudioStream, { end: true }) .on('error', function (error) { console.log('error', error); @@ -3223,7 +2924,11 @@ export class BaileysStartupService extends ChannelStartupService { if (Buffer.isBuffer(convert)) { const result = this.sendMessageWithTyping( data.number, - { audio: convert, ptt: true, mimetype: 'audio/ogg; codecs=opus' }, + { + audio: convert, + ptt: true, + mimetype: 'audio/ogg; codecs=opus', + }, { presence: 'recording', delay: data?.delay }, isIntegration, ); @@ -3262,19 +2967,41 @@ export class BaileysStartupService extends ChannelStartupService { call: () => toString({ display_text: button.displayText, phone_number: button.phoneNumber }), reply: () => toString({ display_text: button.displayText, id: button.id }), copy: () => toString({ display_text: button.displayText, copy_code: button.copyCode }), - url: () => toString({ display_text: button.displayText, url: button.url, merchant_url: button.url }), + url: () => + toString({ + display_text: button.displayText, + url: button.url, + merchant_url: button.url, + }), pix: () => toString({ currency: button.currency, - total_amount: { value: 0, offset: 100 }, + total_amount: { + value: 0, + offset: 100, + }, reference_id: this.generateRandomId(), type: 'physical-goods', order: { status: 'pending', - subtotal: { value: 0, offset: 100 }, + subtotal: { + value: 0, + offset: 100, + }, order_type: 'ORDER', items: [ - { name: '', amount: { value: 0, offset: 100 }, quantity: 0, sale_amount: { value: 0, offset: 100 } }, + { + name: '', + amount: { + value: 0, + offset: 100, + }, + quantity: 0, + sale_amount: { + value: 0, + offset: 100, + }, + }, ], }, payment_settings: [ @@ -3343,8 +3070,16 @@ export class BaileysStartupService extends ChannelStartupService { message: { interactiveMessage: { nativeFlowMessage: { - buttons: [{ name: this.mapType.get('pix'), buttonParamsJson: this.toJSONString(data.buttons[0]) }], - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), + buttons: [ + { + name: this.mapType.get('pix'), + buttonParamsJson: this.toJSONString(data.buttons[0]), + }, + ], + messageParamsJson: JSON.stringify({ + from: 'api', + templateId: v4(), + }), }, }, }, @@ -3362,12 +3097,18 @@ export class BaileysStartupService extends ChannelStartupService { const generate = await (async () => { if (data?.thumbnailUrl) { - return await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }); + return await this.prepareMediaMessage({ + mediatype: 'image', + media: data.thumbnailUrl, + }); } })(); const buttons = data.buttons.map((value) => { - return { name: this.mapType.get(value.type), buttonParamsJson: this.toJSONString(value) }; + return { + name: this.mapType.get(value.type), + buttonParamsJson: this.toJSONString(value), + }; }); const message: proto.IMessage = { @@ -3385,7 +3126,9 @@ export class BaileysStartupService extends ChannelStartupService { return t; })(), }, - footer: { text: data?.footer }, + footer: { + text: data?.footer, + }, header: (() => { if (generate?.message?.imageMessage) { return { @@ -3396,7 +3139,10 @@ export class BaileysStartupService extends ChannelStartupService { })(), nativeFlowMessage: { buttons: buttons, - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), + messageParamsJson: JSON.stringify({ + from: 'api', + templateId: v4(), + }), }, }, }, @@ -3484,12 +3230,18 @@ export class BaileysStartupService extends ChannelStartupService { }; if (data.contact.length === 1) { - message.contactMessage = { displayName: data.contact[0].fullName, vcard: vcard(data.contact[0]) }; + message.contactMessage = { + displayName: data.contact[0].fullName, + vcard: vcard(data.contact[0]), + }; } else { message.contactsArrayMessage = { displayName: `${data.contact.length} contacts`, contacts: data.contact.map((contact) => { - return { displayName: contact.fullName, vcard: vcard(contact) }; + return { + displayName: contact.fullName, + vcard: vcard(contact), + }; }), }; } @@ -3499,7 +3251,10 @@ export class BaileysStartupService extends ChannelStartupService { public async reactionMessage(data: SendReactionDto) { return await this.sendMessageWithTyping(data.key.remoteJid, { - reactionMessage: { key: data.key, text: data.reaction }, + reactionMessage: { + key: data.key, + text: data.reaction, + }, }); } @@ -3509,7 +3264,11 @@ export class BaileysStartupService extends ChannelStartupService { groups: { number: string; jid: string }[]; broadcast: { number: string; jid: string }[]; users: { number: string; jid: string; name?: string }[]; - } = { groups: [], broadcast: [], users: [] }; + } = { + groups: [], + broadcast: [], + users: [], + }; data.numbers.forEach((number) => { const jid = createJid(number); @@ -3534,68 +3293,46 @@ export class BaileysStartupService extends ChannelStartupService { const group = await this.findGroup({ groupJid: jid }, 'inner'); if (!group) { - return new OnWhatsAppDto(jid, false, number); + new OnWhatsAppDto(jid, false, number); } - return new OnWhatsAppDto(group.id, true, number, group?.subject); + return new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject); }), ); onWhatsapp.push(...groups); // USERS const contacts: any[] = await this.prismaRepository.contact.findMany({ - where: { instanceId: this.instanceId, remoteJid: { in: jids.users.map(({ jid }) => jid) } }, + where: { + instanceId: this.instanceId, + remoteJid: { + in: jids.users.map(({ jid }) => jid), + }, + }, }); - // Unified cache verification for all numbers (normal and LID) const numbersToVerify = jids.users.map(({ jid }) => jid.replace('+', '')); - // Get all numbers from cache const cachedNumbers = await getOnWhatsappCache(numbersToVerify); + const filteredNumbers = numbersToVerify.filter( + (jid) => !cachedNumbers.some((cached) => cached.jidOptions.includes(jid)), + ); - // Separate numbers that are and are not in cache - const cachedJids = new Set(cachedNumbers.flatMap((cached) => cached.jidOptions)); - const numbersNotInCache = numbersToVerify.filter((jid) => !cachedJids.has(jid)); - - // Only call Baileys for normal numbers (@s.whatsapp.net) that are not in cache - let verify: { jid: string; exists: boolean }[] = []; - const normalNumbersNotInCache = numbersNotInCache.filter((jid) => !jid.includes('@lid')); - - if (normalNumbersNotInCache.length > 0) { - this.logger.verbose(`Checking ${normalNumbersNotInCache.length} numbers via Baileys (not found in cache)`); - verify = await this.client.onWhatsApp(...normalNumbersNotInCache); - } - - const verifiedUsers = await Promise.all( + const verify = await this.client.onWhatsApp(...filteredNumbers); + const users: OnWhatsAppDto[] = await Promise.all( jids.users.map(async (user) => { - // Try to get from cache first (works for all: normal and LID) - const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', ''))); + let numberVerified: (typeof verify)[0] | null = null; + const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', ''))); if (cached) { - this.logger.verbose(`Number ${user.number} found in cache`); - return new OnWhatsAppDto( - cached.remoteJid, - true, - user.number, - contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName, - cached.lid || (cached.remoteJid.includes('@lid') ? 'lid' : undefined), - ); - } - - // If it's a LID number and not in cache, consider it valid - if (user.jid.includes('@lid')) { - return new OnWhatsAppDto( - user.jid, - true, - user.number, - contacts.find((c) => c.remoteJid === user.jid)?.pushName, - 'lid', - ); + return { + exists: true, + jid: cached.remoteJid, + name: contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName, + number: user.number, + }; } - // If not in cache and is a normal number, use Baileys verification - let numberVerified: (typeof verify)[0] | null = null; - // Brazilian numbers if (user.number.startsWith('55')) { const numberWithDigit = @@ -3615,7 +3352,7 @@ export class BaileysStartupService extends ChannelStartupService { if (!numberVerified && (user.number.startsWith('52') || user.number.startsWith('54'))) { let prefix = ''; if (user.number.startsWith('52')) { - prefix = '1'; + prefix = ''; } if (user.number.startsWith('54')) { prefix = '9'; @@ -3639,36 +3376,18 @@ export class BaileysStartupService extends ChannelStartupService { const numberJid = numberVerified?.jid || user.jid; - return new OnWhatsAppDto( - numberJid, - !!numberVerified?.exists, - user.number, - contacts.find((c) => c.remoteJid === numberJid)?.pushName, - undefined, - ); + return { + exists: !!numberVerified?.exists, + jid: numberJid, + name: contacts.find((c) => c.remoteJid === numberJid)?.pushName, + number: user.number, + }; }), ); - // Combine results - onWhatsapp.push(...verifiedUsers); - - // TODO: Salvar no cache apenas números que NÃO estavam no cache - const numbersToCache = onWhatsapp.filter((user) => { - if (!user.exists) return false; - // Verifica se estava no cache usando jidOptions - const cached = cachedNumbers?.find((cached) => cached.jidOptions.includes(user.jid.replace('+', ''))); - return !cached; - }); + await saveOnWhatsappCache(users.filter((user) => user.exists).map((user) => ({ remoteJid: user.jid }))); - if (numbersToCache.length > 0) { - this.logger.verbose(`Salvando ${numbersToCache.length} números no cache`); - await saveOnWhatsappCache( - numbersToCache.map((user) => ({ - remoteJid: user.jid, - lid: user.lid === 'lid' ? 'lid' : undefined, - })), - ); - } + onWhatsapp.push(...users); return onWhatsapp; } @@ -3677,8 +3396,12 @@ export class BaileysStartupService extends ChannelStartupService { try { const keys: proto.IMessageKey[] = []; data.readMessages.forEach((read) => { - if (isJidGroup(read.remoteJid) || isPnUser(read.remoteJid)) { - keys.push({ remoteJid: read.remoteJid, fromMe: read.fromMe, id: read.id }); + if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { + keys.push({ + remoteJid: read.remoteJid, + fromMe: read.fromMe, + id: read.id, + }); } }); await this.client.readMessages(keys); @@ -3689,11 +3412,18 @@ export class BaileysStartupService extends ChannelStartupService { } public async getLastMessage(number: string) { - const where: any = { key: { remoteJid: number }, instanceId: this.instance.id }; + const where: any = { + key: { + remoteJid: number, + }, + instanceId: this.instance.id, + }; const messages = await this.prismaRepository.message.findMany({ where, - orderBy: { messageTimestamp: 'desc' }, + orderBy: { + messageTimestamp: 'desc', + }, take: 1, }); @@ -3729,9 +3459,18 @@ export class BaileysStartupService extends ChannelStartupService { throw new NotFoundException('Last message not found'); } - await this.client.chatModify({ archive: data.archive, lastMessages: [last_message] }, createJid(number)); + await this.client.chatModify( + { + archive: data.archive, + lastMessages: [last_message], + }, + createJid(number), + ); - return { chatId: number, archived: true }; + return { + chatId: number, + archived: true, + }; } catch (error) { throw new InternalServerErrorException({ archived: false, @@ -3757,9 +3496,18 @@ export class BaileysStartupService extends ChannelStartupService { throw new NotFoundException('Last message not found'); } - await this.client.chatModify({ markRead: false, lastMessages: [last_message] }, createJid(number)); + await this.client.chatModify( + { + markRead: false, + lastMessages: [last_message], + }, + createJid(number), + ); - return { chatId: number, markedChatUnread: true }; + return { + chatId: number, + markedChatUnread: true, + }; } catch (error) { throw new InternalServerErrorException({ markedChatUnread: false, @@ -3775,38 +3523,34 @@ export class BaileysStartupService extends ChannelStartupService { const messageId = response.message?.protocolMessage?.key?.id; if (messageId) { const isLogicalDeleted = configService.get('DATABASE').DELETE_DATA.LOGICAL_MESSAGE_DELETE; - let message = await this.prismaRepository.message.findFirst({ - where: { key: { path: ['id'], equals: messageId } }, + let message = await this.prismaRepository.message.findUnique({ + where: { id: messageId }, }); if (isLogicalDeleted) { if (!message) return response; const existingKey = typeof message?.key === 'object' && message.key !== null ? message.key : {}; message = await this.prismaRepository.message.update({ - where: { id: message.id }, - data: { key: { ...existingKey, deleted: true }, status: 'DELETED' }, + where: { id: messageId }, + data: { + key: { + ...existingKey, + deleted: true, + }, + }, }); - if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) { - const messageUpdate: any = { - messageId: message.id, - keyId: messageId, - remoteJid: response.key.remoteJid, - fromMe: response.key.fromMe, - participant: response.key?.participant, - status: 'DELETED', - instanceId: this.instanceId, - }; - await this.prismaRepository.messageUpdate.create({ data: messageUpdate }); - } } else { - if (!message) return response; - await this.prismaRepository.message.deleteMany({ where: { id: message.id } }); + await this.prismaRepository.message.deleteMany({ + where: { + id: messageId, + }, + }); } this.sendDataWebhook(Events.MESSAGES_DELETE, { id: message.id, instanceId: message.instanceId, key: message.key, messageType: message.messageType, - status: 'DELETED', + status: message.status, source: message.source, messageTimestamp: message.messageTimestamp, pushName: message.pushName, @@ -3822,18 +3566,6 @@ export class BaileysStartupService extends ChannelStartupService { } } - public async mapMediaType(mediaType) { - const map = { - imageMessage: 'image', - videoMessage: 'video', - documentMessage: 'document', - stickerMessage: 'sticker', - audioMessage: 'audio', - ptvMessage: 'video', - }; - return map[mediaType] || null; - } - public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) { try { const m = data?.message; @@ -3851,87 +3583,37 @@ export class BaileysStartupService extends ChannelStartupService { } } - if ('messageContextInfo' in msg.message && Object.keys(msg.message).length === 1) { - this.logger.verbose('Message contains only messageContextInfo, skipping media processing'); - return null; - } - let mediaMessage: any; let mediaType: string; - if (msg.message?.templateMessage) { - const template = - msg.message.templateMessage.hydratedTemplate || msg.message.templateMessage.hydratedFourRowTemplate; - - for (const type of TypeMediaMessage) { - if (template[type]) { - mediaMessage = template[type]; - mediaType = type; - msg.message = { [type]: { ...template[type], url: template[type].staticUrl } }; - break; - } - } - - if (!mediaMessage) { - throw 'Template message does not contain a supported media type'; - } - } else { - for (const type of TypeMediaMessage) { - mediaMessage = msg.message[type]; - if (mediaMessage) { - mediaType = type; - break; - } + for (const type of TypeMediaMessage) { + mediaMessage = msg.message[type]; + if (mediaMessage) { + mediaType = type; + break; } + } - if (!mediaMessage) { - throw 'The message is not of the media type'; - } + if (!mediaMessage) { + throw 'The message is not of the media type'; } if (typeof mediaMessage['mediaKey'] === 'object') { - msg.message[mediaType].mediaKey = Uint8Array.from(Object.values(mediaMessage['mediaKey'])); + msg.message = JSON.parse(JSON.stringify(msg.message)); } - let buffer: Buffer; - - try { - buffer = await downloadMediaMessage( - { key: msg?.key, message: msg?.message }, - 'buffer', - {}, - { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, - ); - } catch { - this.logger.error('Download Media failed, trying to retry in 5 seconds...'); - await new Promise((resolve) => setTimeout(resolve, 5000)); - const mediaType = Object.keys(msg.message).find((key) => key.endsWith('Message')); - if (!mediaType) throw new Error('Could not determine mediaType for fallback'); - - try { - const media = await downloadContentFromMessage( - { - mediaKey: msg.message?.[mediaType]?.mediaKey, - directPath: msg.message?.[mediaType]?.directPath, - url: `https://mmg.whatsapp.net${msg?.message?.[mediaType]?.directPath}`, - }, - await this.mapMediaType(mediaType), - {}, - ); - const chunks = []; - for await (const chunk of media) { - chunks.push(chunk); - } - buffer = Buffer.concat(chunks); - this.logger.info('Download Media with downloadContentFromMessage was successful!'); - } catch (fallbackErr) { - this.logger.error('Download Media with downloadContentFromMessage also failed!'); - throw fallbackErr; - } - } + const buffer = await downloadMediaMessage( + { key: msg?.key, message: msg?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + }, + ); const typeMessage = getContentType(msg.message); - const ext = mimeTypes.extension(mediaMessage?.['mimetype']); + const ext = mime.getExtension(mediaMessage?.['mimetype']); const fileName = mediaMessage?.['fileName'] || `${msg.key.id}.${ext}` || `${v4()}.${ext}`; if (convertToMp4 && typeMessage === 'audioMessage') { @@ -3966,7 +3648,11 @@ export class BaileysStartupService extends ChannelStartupService { mediaType, fileName, caption: mediaMessage['caption'], - size: { fileLength: mediaMessage['fileLength'], height: mediaMessage['height'], width: mediaMessage['width'] }, + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], + }, mimetype: mediaMessage['mimetype'], base64: buffer.toString('base64'), buffer: getBuffer ? buffer : null, @@ -4027,10 +3713,17 @@ export class BaileysStartupService extends ChannelStartupService { if (!profile) { const info = await this.whatsappNumber({ numbers: [jid] }); - return { isBusiness: false, message: 'Not is business profile', ...info?.shift() }; + return { + isBusiness: false, + message: 'Not is business profile', + ...info?.shift(), + }; } - return { isBusiness: true, ...profile }; + return { + isBusiness: true, + ...profile, + }; } catch (error) { throw new InternalServerErrorException('Error updating profile name', error.toString()); } @@ -4061,11 +3754,11 @@ export class BaileysStartupService extends ChannelStartupService { let pic: WAMediaUpload; if (isURL(picture)) { const timestamp = new Date().getTime(); - const parsedURL = new URL(picture); - parsedURL.searchParams.set('timestamp', timestamp.toString()); - const url = parsedURL.toString(); + const url = `${picture}?timestamp=${timestamp}`; - let config: any = { responseType: 'arraybuffer' }; + let config: any = { + responseType: 'arraybuffer', + }; if (this.localProxy?.enabled) { config = { @@ -4131,22 +3824,26 @@ export class BaileysStartupService extends ChannelStartupService { private async formatUpdateMessage(data: UpdateMessageDto) { try { - if (!this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { - return data; - } - const msg: any = await this.getMessage(data.key, true); if (msg?.messageType === 'conversation' || msg?.messageType === 'extendedTextMessage') { - return { text: data.text }; + return { + text: data.text, + }; } if (msg?.messageType === 'imageMessage') { - return { image: msg?.message?.imageMessage, caption: data.text }; + return { + image: msg?.message?.imageMessage, + caption: data.text, + }; } if (msg?.messageType === 'videoMessage') { - return { video: msg?.message?.videoMessage, caption: data.text }; + return { + video: msg?.message?.videoMessage, + caption: data.text, + }; } return null; @@ -4167,85 +3864,22 @@ export class BaileysStartupService extends ChannelStartupService { } try { - const oldMessage: any = await this.getMessage(data.key, true); - if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { - if (!oldMessage) throw new NotFoundException('Message not found'); - if (oldMessage?.key?.remoteJid !== jid) { - throw new BadRequestException('RemoteJid does not match'); - } - if (oldMessage?.messageTimestamp > Date.now() + 900000) { - // 15 minutes in milliseconds - throw new BadRequestException('Message is older than 15 minutes'); - } - } - - const messageSent = await this.client.sendMessage(jid, { ...(options as any), edit: data.key }); - if (messageSent) { - const editedMessage = - messageSent?.message?.protocolMessage || messageSent?.message?.editedMessage?.message?.protocolMessage; - - if (editedMessage) { - this.sendDataWebhook(Events.SEND_MESSAGE_UPDATE, editedMessage); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) - this.chatwootService.eventWhatsapp( - 'send.message.update', - { instanceName: this.instance.name, instanceId: this.instance.id }, - editedMessage, - ); - - const messageId = messageSent.message?.protocolMessage?.key?.id; - if (messageId && this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { - let message = await this.prismaRepository.message.findFirst({ - where: { key: { path: ['id'], equals: messageId } }, - }); - if (!message) throw new NotFoundException('Message not found'); - - if (!(message.key.valueOf() as any).fromMe) { - new BadRequestException('You cannot edit others messages'); - } - if ((message.key.valueOf() as any)?.deleted) { - new BadRequestException('You cannot edit deleted messages'); - } - - if (oldMessage.messageType === 'conversation' || oldMessage.messageType === 'extendedTextMessage') { - oldMessage.message.conversation = data.text; - } else { - oldMessage.message[oldMessage.messageType].caption = data.text; - } - message = await this.prismaRepository.message.update({ - where: { id: message.id }, - data: { - message: oldMessage.message, - status: 'EDITED', - messageTimestamp: Math.floor(Date.now() / 1000), // Convert to int32 by dividing by 1000 to get seconds - }, - }); - - if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) { - const messageUpdate: any = { - messageId: message.id, - keyId: messageId, - remoteJid: messageSent.key.remoteJid, - fromMe: messageSent.key.fromMe, - participant: messageSent.key?.participant, - status: 'EDITED', - instanceId: this.instanceId, - }; - await this.prismaRepository.messageUpdate.create({ data: messageUpdate }); - } - } - } - } - - return messageSent; + return await this.client.sendMessage(jid, { + ...(options as any), + edit: data.key, + }); } catch (error) { this.logger.error(error); - throw error; + throw new BadRequestException(error.toString()); } } public async fetchLabels(): Promise { - const labels = await this.prismaRepository.label.findMany({ where: { instanceId: this.instanceId } }); + const labels = await this.prismaRepository.label.findMany({ + where: { + instanceId: this.instanceId, + }, + }); return labels.map((label) => ({ color: label.color, @@ -4268,13 +3902,11 @@ export class BaileysStartupService extends ChannelStartupService { try { if (data.action === 'add') { await this.client.addChatLabel(contact.jid, data.labelId); - await this.addLabel(data.labelId, this.instanceId, contact.jid); return { numberJid: contact.jid, labelId: data.labelId, add: true }; } if (data.action === 'remove') { await this.client.removeChatLabel(contact.jid, data.labelId); - await this.removeLabel(data.labelId, this.instanceId, contact.jid); return { numberJid: contact.jid, labelId: data.labelId, remove: true }; } @@ -4292,7 +3924,10 @@ export class BaileysStartupService extends ChannelStartupService { if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) { this.logger.verbose(`Updating cache for group: ${groupJid}`); - await groupMetadataCache.set(groupJid, { timestamp: Date.now(), data: meta }); + await groupMetadataCache.set(groupJid, { + timestamp: Date.now(), + data: meta, + }); } return meta; @@ -4338,7 +3973,11 @@ export class BaileysStartupService extends ChannelStartupService { } if (create?.promoteParticipants) { - await this.updateGParticipant({ groupJid: id, action: 'promote', participants: participants }); + await this.updateGParticipant({ + groupJid: id, + action: 'promote', + participants: participants, + }); } const group = await this.client.groupMetadata(id); @@ -4355,11 +3994,11 @@ export class BaileysStartupService extends ChannelStartupService { let pic: WAMediaUpload; if (isURL(picture.image)) { const timestamp = new Date().getTime(); - const parsedURL = new URL(picture.image); - parsedURL.searchParams.set('timestamp', timestamp.toString()); - const url = parsedURL.toString(); + const url = `${picture.image}?timestamp=${timestamp}`; - let config: any = { responseType: 'arraybuffer' }; + let config: any = { + responseType: 'arraybuffer', + }; if (this.localProxy?.enabled) { config = { @@ -4492,7 +4131,7 @@ export class BaileysStartupService extends ChannelStartupService { public async inviteInfo(id: GroupInvite) { try { return await this.client.groupGetInviteInfo(id.inviteCode); - } catch { + } catch (error) { throw new NotFoundException('No invite info', id.inviteCode); } } @@ -4508,14 +4147,16 @@ export class BaileysStartupService extends ChannelStartupService { const msg = `${description}\n\n${inviteUrl}`; - const message = { conversation: msg }; + const message = { + conversation: msg, + }; for await (const number of numbers) { await this.sendMessageWithTyping(number, message); } return { send: true, inviteUrl }; - } catch { + } catch (error) { throw new NotFoundException('No send invite'); } } @@ -4542,7 +4183,12 @@ export class BaileysStartupService extends ChannelStartupService { try { const participants = (await this.client.groupMetadata(id.groupJid)).participants; const contacts = await this.prismaRepository.contact.findMany({ - where: { instanceId: this.instanceId, remoteJid: { in: participants.map((p) => p.id) } }, + where: { + instanceId: this.instanceId, + remoteJid: { + in: participants.map((p) => p.id), + }, + }, }); const parsedParticipants = participants.map((participant) => { const contact = contacts.find((c) => c.remoteJid === participant.id); @@ -4605,68 +4251,22 @@ export class BaileysStartupService extends ChannelStartupService { throw new BadRequestException('Unable to leave the group', error.toString()); } } - public async templateMessage() { throw new Error('Method not available in the Baileys service'); } - private deserializeMessageBuffers(obj: any): any { - if (obj === null || obj === undefined) { - return obj; - } - - if (typeof obj === 'object' && !Array.isArray(obj) && !Buffer.isBuffer(obj)) { - const keys = Object.keys(obj); - const isIndexedObject = keys.every((key) => !isNaN(Number(key))); - - if (isIndexedObject && keys.length > 0) { - const values = keys.sort((a, b) => Number(a) - Number(b)).map((key) => obj[key]); - return new Uint8Array(values); - } - } - - // Is Buffer?, converter to Uint8Array - if (Buffer.isBuffer(obj)) { - return new Uint8Array(obj); - } - - // Process arrays recursively - if (Array.isArray(obj)) { - return obj.map((item) => this.deserializeMessageBuffers(item)); - } - - // Process objects recursively - if (typeof obj === 'object') { - const converted: any = {}; - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - converted[key] = this.deserializeMessageBuffers(obj[key]); - } - } - return converted; - } - - return obj; - } - private prepareMessage(message: proto.IWebMessageInfo): any { const contentType = getContentType(message.message); const contentMsg = message?.message[contentType] as any; const messageRaw = { - key: message.key, // Save key exactly as it comes from Baileys - pushName: - message.pushName || - (message.key.fromMe - ? 'Você' - : message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)), + key: message.key, + pushName: message.pushName, status: status[message.status], - message: this.deserializeMessageBuffers({ ...message.message }), - contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo), + message: { ...message.message }, + contextInfo: contentMsg?.contextInfo, messageType: contentType || 'unknown', - messageTimestamp: Long.isLong(message.messageTimestamp) - ? message.messageTimestamp.toNumber() - : (message.messageTimestamp as number), + messageTimestamp: message.messageTimestamp as number, instanceId: this.instanceId, source: getDevice(message.key.id), }; @@ -4687,19 +4287,6 @@ export class BaileysStartupService extends ChannelStartupService { delete messageRaw.message.documentWithCaptionMessage; } - const quotedMessage = messageRaw?.contextInfo?.quotedMessage; - if (quotedMessage) { - if (quotedMessage.extendedTextMessage) { - quotedMessage.conversation = quotedMessage.extendedTextMessage.text; - delete quotedMessage.extendedTextMessage; - } - - if (quotedMessage.documentWithCaptionMessage) { - quotedMessage.documentMessage = quotedMessage.documentWithCaptionMessage.message.documentMessage; - delete quotedMessage.documentWithCaptionMessage; - } - } - return messageRaw; } @@ -4709,22 +4296,7 @@ export class BaileysStartupService extends ChannelStartupService { const prepare = (message: any) => this.prepareMessage(message); this.chatwootService.syncLostMessages({ instanceName: this.instance.name }, chatwootConfig, prepare); - // Generate ID for this cron task and store in cache - const cronId = cuid(); - const cronKey = `chatwoot:syncLostMessages`; - await this.chatwootService.getCache()?.hSet(cronKey, this.instance.name, cronId); - const task = cron.schedule('0,30 * * * *', async () => { - // Check ID before executing (only if cache is available) - const cache = this.chatwootService.getCache(); - if (cache) { - const storedId = await cache.hGet(cronKey, this.instance.name); - if (storedId && storedId !== cronId) { - this.logger.info(`Stopping syncChatwootLostMessages cron - ID mismatch: ${cronId} vs ${storedId}`); - task.stop(); - return; - } - } this.chatwootService.syncLostMessages({ instanceName: this.instance.name }, chatwootConfig, prepare); }); task.start(); @@ -4734,23 +4306,26 @@ export class BaileysStartupService extends ChannelStartupService { private async updateMessagesReadedByTimestamp(remoteJid: string, timestamp?: number): Promise { if (timestamp === undefined || timestamp === null) return 0; - // Use raw SQL to avoid JSON path issues - const result = await this.prismaRepository.$executeRaw` - UPDATE "Message" - SET "status" = ${status[4]} - WHERE "instanceId" = ${this.instanceId} - AND "key"->>'remoteJid' = ${remoteJid} - AND ("key"->>'fromMe')::boolean = false - AND "messageTimestamp" <= ${timestamp} - AND ("status" IS NULL OR "status" = ${status[3]}) - `; + const result = await this.prismaRepository.message.updateMany({ + where: { + AND: [ + { key: { path: ['remoteJid'], equals: remoteJid } }, + { key: { path: ['fromMe'], equals: false } }, + { messageTimestamp: { lte: timestamp } }, + { + OR: [{ status: null }, { status: status[3] }], + }, + ], + }, + data: { status: status[4] }, + }); if (result) { - if (result > 0) { + if (result.count > 0) { this.updateChatUnreadMessages(remoteJid); } - return result; + return result.count; } return 0; @@ -4759,364 +4334,24 @@ export class BaileysStartupService extends ChannelStartupService { private async updateChatUnreadMessages(remoteJid: string): Promise { const [chat, unreadMessages] = await Promise.all([ this.prismaRepository.chat.findFirst({ where: { remoteJid } }), - // Use raw SQL to avoid JSON path issues - this.prismaRepository.$queryRaw` - SELECT COUNT(*)::int as count FROM "Message" - WHERE "instanceId" = ${this.instanceId} - AND "key"->>'remoteJid' = ${remoteJid} - AND ("key"->>'fromMe')::boolean = false - AND "status" = ${status[3]} - `.then((result: any[]) => result[0]?.count || 0), + this.prismaRepository.message.count({ + where: { + AND: [ + { key: { path: ['remoteJid'], equals: remoteJid } }, + { key: { path: ['fromMe'], equals: false } }, + { status: { equals: status[3] } }, + ], + }, + }), ]); if (chat && chat.unreadMessages !== unreadMessages) { - await this.prismaRepository.chat.update({ where: { id: chat.id }, data: { unreadMessages } }); + await this.prismaRepository.chat.update({ + where: { id: chat.id }, + data: { unreadMessages }, + }); } return unreadMessages; } - - private async addLabel(labelId: string, instanceId: string, chatId: string) { - const id = cuid(); - - await this.prismaRepository.$executeRawUnsafe( - `INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt") - VALUES ($4, $2, $3, to_jsonb(ARRAY[$1]::text[]), NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid") - DO - UPDATE - SET "labels" = ( - SELECT to_jsonb(array_agg(DISTINCT elem)) - FROM ( - SELECT jsonb_array_elements_text("Chat"."labels") AS elem - UNION - SELECT $1::text AS elem - ) sub - ), - "updatedAt" = NOW();`, - labelId, - instanceId, - chatId, - id, - ); - } - - private async removeLabel(labelId: string, instanceId: string, chatId: string) { - const id = cuid(); - - await this.prismaRepository.$executeRawUnsafe( - `INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt") - VALUES ($4, $2, $3, '[]'::jsonb, NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid") - DO - UPDATE - SET "labels" = COALESCE ( - ( - SELECT jsonb_agg(elem) - FROM jsonb_array_elements_text("Chat"."labels") AS elem - WHERE elem <> $1 - ), - '[]'::jsonb - ), - "updatedAt" = NOW();`, - labelId, - instanceId, - chatId, - 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[]) { - const response = await this.client.assertSessions(jids); - - 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; - } - - //Business Controller - public async fetchCatalog(instanceName: string, data: getCollectionsDto) { - const jid = data.number ? createJid(data.number) : this.client?.user?.id; - const limit = data.limit || 10; - const cursor = null; - - const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - - if (!onWhatsapp.exists) { - throw new BadRequestException(onWhatsapp); - } - - try { - const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - const business = await this.fetchBusinessProfile(info?.jid); - - let catalog = await this.getCatalog({ jid: info?.jid, limit, cursor }); - let nextPageCursor = catalog.nextPageCursor; - let nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; - let pagination = nextPageCursorJson?.pagination_cursor - ? JSON.parse(atob(nextPageCursorJson.pagination_cursor)) - : null; - let fetcherHasMore = pagination?.fetcher_has_more === true ? true : false; - - let productsCatalog = catalog.products || []; - let countLoops = 0; - while (fetcherHasMore && countLoops < 4) { - catalog = await this.getCatalog({ jid: info?.jid, limit, cursor: nextPageCursor }); - nextPageCursor = catalog.nextPageCursor; - nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; - pagination = nextPageCursorJson?.pagination_cursor - ? JSON.parse(atob(nextPageCursorJson.pagination_cursor)) - : null; - fetcherHasMore = pagination?.fetcher_has_more === true ? true : false; - productsCatalog = [...productsCatalog, ...catalog.products]; - countLoops++; - } - - return { - wuid: info?.jid || jid, - numberExists: info?.exists, - isBusiness: business.isBusiness, - catalogLength: productsCatalog.length, - catalog: productsCatalog, - }; - } catch (error) { - console.log(error); - return { wuid: jid, name: null, isBusiness: false }; - } - } - - public async getCatalog({ - jid, - limit, - cursor, - }: GetCatalogOptions): Promise<{ products: Product[]; nextPageCursor: string | undefined }> { - try { - jid = jid ? createJid(jid) : this.instance.wuid; - - const catalog = await this.client.getCatalog({ jid, limit: limit, cursor: cursor }); - - if (!catalog) { - return { products: undefined, nextPageCursor: undefined }; - } - - return catalog; - } catch (error) { - throw new InternalServerErrorException('Error getCatalog', error.toString()); - } - } - - public async fetchCollections(instanceName: string, data: getCollectionsDto) { - const jid = data.number ? createJid(data.number) : this.client?.user?.id; - const limit = data.limit <= 20 ? data.limit : 20; //(tem esse limite, não sei porque) - - const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - - if (!onWhatsapp.exists) { - throw new BadRequestException(onWhatsapp); - } - - try { - const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - const business = await this.fetchBusinessProfile(info?.jid); - const collections = await this.getCollections(info?.jid, limit); - - return { - wuid: info?.jid || jid, - name: info?.name, - numberExists: info?.exists, - isBusiness: business.isBusiness, - collectionsLength: collections?.length, - collections: collections, - }; - } catch { - return { wuid: jid, name: null, isBusiness: false }; - } - } - - public async getCollections(jid?: string | undefined, limit?: number): Promise { - try { - jid = jid ? createJid(jid) : this.instance.wuid; - - const result = await this.client.getCollections(jid, limit); - - if (!result) { - return [{ id: undefined, name: undefined, products: [], status: undefined }]; - } - - return result.collections; - } catch (error) { - throw new InternalServerErrorException('Error getCatalog', error.toString()); - } - } - - public async fetchMessages(query: Query) { - const keyFilters = query?.where?.key as ExtendedIMessageKey; - - const timestampFilter = {}; - if (query?.where?.messageTimestamp) { - if (query.where.messageTimestamp['gte'] && query.where.messageTimestamp['lte']) { - timestampFilter['messageTimestamp'] = { - gte: Math.floor(new Date(query.where.messageTimestamp['gte']).getTime() / 1000), - lte: Math.floor(new Date(query.where.messageTimestamp['lte']).getTime() / 1000), - }; - } - } - - const count = await this.prismaRepository.message.count({ - where: { - instanceId: this.instanceId, - id: query?.where?.id, - source: query?.where?.source, - messageType: query?.where?.messageType, - ...timestampFilter, - AND: [ - keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {}, - keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {}, - keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {}, - { - OR: [ - keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {}, - keyFilters?.remoteJidAlt ? { key: { path: ['remoteJidAlt'], equals: keyFilters?.remoteJidAlt } } : {}, - ], - }, - ], - }, - }); - - if (!query?.offset) { - query.offset = 50; - } - - if (!query?.page) { - query.page = 1; - } - - const messages = await this.prismaRepository.message.findMany({ - where: { - instanceId: this.instanceId, - id: query?.where?.id, - source: query?.where?.source, - messageType: query?.where?.messageType, - ...timestampFilter, - AND: [ - keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {}, - keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {}, - keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {}, - { - OR: [ - keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {}, - keyFilters?.remoteJidAlt ? { key: { path: ['remoteJidAlt'], equals: keyFilters?.remoteJidAlt } } : {}, - ], - }, - ], - }, - orderBy: { messageTimestamp: 'desc' }, - skip: query.offset * (query?.page === 1 ? 0 : (query?.page as number) - 1), - take: query.offset, - select: { - id: true, - key: true, - pushName: true, - messageType: true, - message: true, - messageTimestamp: true, - instanceId: true, - source: true, - contextInfo: true, - MessageUpdate: { select: { status: true } }, - }, - }); - - const formattedMessages = messages.map((message) => { - const messageKey = message.key as { fromMe: boolean; remoteJid: string; id: string; participant?: string }; - - if (!message.pushName) { - if (messageKey.fromMe) { - message.pushName = 'Você'; - } else if (message.contextInfo) { - const contextInfo = message.contextInfo as { participant?: string }; - if (contextInfo.participant) { - message.pushName = contextInfo.participant.split('@')[0]; - } else if (messageKey.participant) { - message.pushName = messageKey.participant.split('@')[0]; - } - } - } - - return message; - }); - - return { - messages: { - total: count, - pages: Math.ceil(count / query.offset), - currentPage: query.page, - records: formattedMessages, - }, - }; - } } diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 906fff188..b6898dbf5 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -24,6 +24,7 @@ import i18next from '@utils/i18n'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; import { WAMessageContent, WAMessageKey } from 'baileys'; +import { ExtendedIMessageKey } from '../../channel/whatsapp/whatsapp.baileys.service'; import dayjs from 'dayjs'; import FormData from 'form-data'; import { Jimp, JimpMime } from 'jimp'; @@ -41,6 +42,11 @@ interface ChatwootMessage { isRead?: boolean; } +interface MessageBodyWithExtendedKey { + key: ExtendedIMessageKey; + [key: string]: any; +} + export class ChatwootService { private readonly logger = new Logger('ChatwootService'); @@ -629,7 +635,7 @@ export class ChatwootService { return filterPayload; } - public async createConversation(instance: InstanceDto, body: any) { + public async createConversation(instance: InstanceDto, body: MessageBodyWithExtendedKey) { const isLid = body.key.addressingMode === 'lid'; const isGroup = body.key.remoteJid.endsWith('@g.us'); const phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid; diff --git a/src/api/types/wa.types.ts b/src/api/types/wa.types.ts index 8f7c6a390..b33dc746e 100644 --- a/src/api/types/wa.types.ts +++ b/src/api/types/wa.types.ts @@ -1,6 +1,20 @@ /* eslint-disable @typescript-eslint/no-namespace */ import { JsonValue } from '@prisma/client/runtime/library'; -import { AuthenticationState, WAConnectionState } from 'baileys'; +import { AuthenticationState, Contact, proto, WAConnectionState } from 'baileys'; + +/** + * LID (Linked Identity Device) extensions for Baileys types. + * The EvolutionAPI Baileys fork adds `remoteJidAlt` to message keys and + * `lidJidAlt` to contacts when the original JID uses the @lid suffix. + * These interfaces provide type-safe access to those fields. + */ +export interface LidMessageKey extends proto.IMessageKey { + remoteJidAlt?: string; +} + +export interface LidContact extends Contact { + lidJidAlt?: string; +} export enum Events { APPLICATION_STARTUP = 'application.startup', @@ -15,7 +29,6 @@ export enum Events { MESSAGES_UPDATE = 'messages.update', MESSAGES_DELETE = 'messages.delete', SEND_MESSAGE = 'send.message', - SEND_MESSAGE_UPDATE = 'send.message.update', CONTACTS_SET = 'contacts.set', CONTACTS_UPSERT = 'contacts.upsert', CONTACTS_UPDATE = 'contacts.update', @@ -52,7 +65,6 @@ export declare namespace wa { pairingCode?: string; authState?: { state: AuthenticationState; saveCreds: () => void }; name?: string; - ownerJid?: string; wuid?: string; profileName?: string; profilePictureUrl?: string; @@ -87,7 +99,6 @@ export declare namespace wa { readMessages?: boolean; readStatus?: boolean; syncFullHistory?: boolean; - wavoipToken?: string; }; export type LocalEvent = { @@ -134,14 +145,21 @@ export declare namespace wa { export type StatusMessage = 'ERROR' | 'PENDING' | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' | 'DELETED' | 'PLAYED'; } -export const TypeMediaMessage = [ - 'imageMessage', - 'documentMessage', - 'audioMessage', - 'videoMessage', - 'stickerMessage', - 'ptvMessage', -]; +/** Resolve a @lid JID to its @s.whatsapp.net alternative when available. */ +export function resolveLidJid(key: LidMessageKey): string { + return key.remoteJid?.includes('@lid') && key.remoteJidAlt + ? key.remoteJidAlt + : key.remoteJid; +} + +/** Resolve a @lid contact ID to its @s.whatsapp.net alternative when available. */ +export function resolveLidContact(contact: LidContact): string { + return contact.id?.includes('@lid') && contact.lidJidAlt + ? contact.lidJidAlt + : contact.id; +} + +export const TypeMediaMessage = ['imageMessage', 'documentMessage', 'audioMessage', 'videoMessage', 'stickerMessage', 'ptvMessage']; export const MessageSubtype = [ 'ephemeralMessage',