diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 2636adbda..6f485e558 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1,4 +1,4 @@ -import { getCollectionsDto } from '@api/dto/business.dto'; +import { getCollectionsDto } from '@api/dto/business.dto'; import { OfferCallDto } from '@api/dto/call.dto'; import { ArchiveChatDto, @@ -59,7 +59,12 @@ import { PrismaRepository, Query } 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, + MessageSubtype, + TypeMediaMessage, + wa, +} from '@api/types/wa.types'; import { CacheEngine } from '@cache/cacheengine'; import { AudioConverter, @@ -75,15 +80,22 @@ import { QrCode, S3, } from '@config/env.config'; -import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions'; +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 { createJid } from '@utils/createJid'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; -import {makeProxyAgent, makeProxyAgentUndici} from '@utils/makeProxyAgent'; -import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; +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'; @@ -159,10 +171,14 @@ export interface ExtendedIMessageKey extends proto.IMessageKey { isViewOnce?: boolean; } -const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine()); +const groupMetadataCache = new CacheService( + new CacheEngine(configService, 'groups').getEngine() +); // Adicione a função getVideoDuration no início do arquivo -async function getVideoDuration(input: Buffer | string | Readable): Promise { +async function getVideoDuration( + input: Buffer | string | Readable +): Promise { const MediaInfoFactory = (await import('mediainfo.js')).default; const mediainfo = await MediaInfoFactory({ format: 'JSON' }); @@ -190,7 +206,9 @@ async function getVideoDuration(input: Buffer | string | Readable): Promise fileSize, readChunk); const jsonResult = JSON.parse(result); - const generalTrack = jsonResult.media.track.find((t: any) => t['@type'] === 'General'); + const generalTrack = jsonResult.media.track.find( + (t: any) => t['@type'] === 'General' + ); const duration = generalTrack.Duration; return Math.round(parseFloat(duration)); @@ -215,7 +233,9 @@ async function getVideoDuration(input: Buffer | string | Readable): Promise fileSize, readChunk); const jsonResult = JSON.parse(result); - const generalTrack = jsonResult.media.track.find((t: any) => t['@type'] === 'General'); + const generalTrack = jsonResult.media.track.find( + (t: any) => t['@type'] === 'General' + ); const duration = generalTrack.Duration; return Math.round(parseFloat(duration)); @@ -231,7 +251,7 @@ export class BaileysStartupService extends ChannelStartupService { public readonly cache: CacheService, public readonly chatwootCache: CacheService, public readonly baileysCache: CacheService, - private readonly providerFiles: ProviderFiles, + private readonly providerFiles: ProviderFiles ) { super(configService, eventEmitter, prismaRepository, chatwootCache); this.instance.qrcode = { count: 0 }; @@ -244,7 +264,10 @@ export class BaileysStartupService extends ChannelStartupService { private authStateProvider: AuthStateProvider; private readonly msgRetryCounterCache: CacheStore = new NodeCache(); - private readonly userDevicesCache: CacheStore = new NodeCache({ stdTTL: 300000, useClones: false }); + private readonly userDevicesCache: CacheStore = new NodeCache({ + stdTTL: 300000, + useClones: false, + }); private endSession = false; private logBaileys = this.configService.get('LOG').BAILEYS; @@ -266,19 +289,28 @@ export class BaileysStartupService extends ChannelStartupService { this.client?.ws?.close(); - 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); + const creds = JSON.parse( + JSON.stringify(data.creds), + BufferJSON.reviver + ); profileName = creds.me?.name || creds.me?.verifiedName; } } @@ -305,19 +337,32 @@ export class BaileysStartupService extends ChannelStartupService { }; } - private async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { + private async connectionUpdate({ + qr, + connection, + lastDisconnect, + }: Partial) { if (qr) { - if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { + if ( + this.instance.qrcode.count === + this.configService.get('QRCODE').LIMIT + ) { this.sendDataWebhook(Events.QRCODE_UPDATED, { message: 'QR code limit reached, please login again', statusCode: DisconnectReason.badSession, }); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { 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, + } ); } @@ -348,7 +393,9 @@ export class BaileysStartupService extends ChannelStartupService { if (this.phoneNumber) { await delay(1000); - this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber); + this.instance.qrcode.pairingCode = await this.client.requestPairingCode( + this.phoneNumber + ); } else { this.instance.qrcode.pairingCode = null; } @@ -363,16 +410,29 @@ 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) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { this.chatwootService.eventWhatsapp( 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, + }, + } ); } }); @@ -380,8 +440,8 @@ export class BaileysStartupService extends ChannelStartupService { qrcodeTerminal.generate(qr, { small: true }, (qrcode) => this.logger.log( `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + - qrcode, - ), + qrcode + ) ); await this.prismaRepository.instance.update({ @@ -393,13 +453,19 @@ export class BaileysStartupService extends ChannelStartupService { if (connection) { this.stateConnection = { state: connection, - statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, + statusReason: + (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, }; } if (connection === 'close') { const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; - const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406]; + const codesToNotReconnect = [ + DisconnectReason.loggedOut, + DisconnectReason.forbidden, + 402, + 406, + ]; const shouldReconnect = !codesToNotReconnect.includes(statusCode); if (shouldReconnect) { await this.connectToWhatsapp(this.phoneNumber); @@ -422,11 +488,14 @@ export class BaileysStartupService extends ChannelStartupService { }, }); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { 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' } ); } @@ -434,7 +503,10 @@ export class BaileysStartupService extends ChannelStartupService { this.client?.ws?.close(); this.client.end(new Error('Close connection')); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, ...this.stateConnection }); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + ...this.stateConnection, + }); } } @@ -452,13 +524,13 @@ export class BaileysStartupService extends ChannelStartupService { ` ┌──────────────────────────────┐ │ CONNECTED TO WHATSAPP │ - └──────────────────────────────┘`.replace(/^ +/gm, ' '), + └──────────────────────────────┘`.replace(/^ +/gm, ' ') ); this.logger.info( ` wuid: ${formattedWuid} name: ${formattedName} - `, + ` ); await this.prismaRepository.instance.update({ @@ -471,11 +543,14 @@ export class BaileysStartupService extends ChannelStartupService { }, }); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { 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(); } @@ -490,7 +565,10 @@ export class BaileysStartupService extends ChannelStartupService { } if (connection === 'connecting') { - this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, ...this.stateConnection }); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + ...this.stateConnection, + }); } } @@ -498,16 +576,17 @@ export class BaileysStartupService extends ChannelStartupService { 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} + SELECT * FROM 'Message' + WHERE 'instanceId' = ${this.instanceId} + AND 'key'->>'id' = ${key.id} `) as proto.IWebMessageInfo[]; if (full) { return webMessageInfo[0]; } if (webMessageInfo[0].message?.pollCreationMessage) { - const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; + const messageSecretBase64 = + webMessageInfo[0].message?.messageContextInfo?.messageSecret; if (typeof messageSecretBase64 === 'string') { const messageSecret = Buffer.from(messageSecretBase64, 'base64'); @@ -550,7 +629,9 @@ export class BaileysStartupService extends ChannelStartupService { private async createClient(number?: string): Promise { this.instance.authState = await this.defineAuthState(); - const session = this.configService.get('CONFIG_SESSION_PHONE'); + const session = this.configService.get( + 'CONFIG_SESSION_PHONE' + ); let browserOptions = {}; @@ -559,7 +640,11 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.info(`Phone number: ${number}`); } else { - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; + const browser: WABrowserDescription = [ + session.CLIENT, + session.NAME, + release(), + ]; browserOptions = { browser }; this.logger.info(`Browser: ${browser}`); @@ -594,7 +679,10 @@ 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) }; + options = { + agent: makeProxyAgent(proxyUrl), + fetchAgent: makeProxyAgent(proxyUrl), + }; } catch { this.localProxy.enabled = false; } @@ -625,11 +713,15 @@ export class BaileysStartupService extends ChannelStartupService { printQRInTerminal: false, auth: { creds: this.instance.authState.state.creds, - keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), + keys: makeCacheableSignalKeyStore( + this.instance.authState.state.keys, + P({ level: 'error' }) as any + ), }, msgRetryCounterCache: this.msgRetryCounterCache, generateHighQualityLinkPreview: true, - getMessage: async (key) => (await this.getMessage(key)) as Promise, + getMessage: async (key) => + (await this.getMessage(key)) as Promise, ...browserOptions, markOnlineOnConnect: this.localSettings.alwaysOnline, retryRequestDelayMs: 350, @@ -645,13 +737,16 @@ export class BaileysStartupService extends ChannelStartupService { } const isGroupJid = this.localSettings.groupsIgnore && isJidGroup(jid); - const isBroadcast = !this.localSettings.readStatus && isJidBroadcast(jid); + const isBroadcast = + !this.localSettings.readStatus && isJidBroadcast(jid); const isNewsletter = isJidNewsletter(jid); return isGroupJid || isBroadcast || isNewsletter; }, syncFullHistory: this.localSettings.syncFullHistory, - shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => { + shouldSyncHistoryMessage: ( + msg: proto.Message.IHistorySyncNotification + ) => { return this.historySyncNotification(msg); }, cachedGroupMetadata: this.getGroupMetadataCache, @@ -659,17 +754,23 @@ export class BaileysStartupService extends ChannelStartupService { transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 }, patchMessageBeforeSending(message) { if ( - message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST + message.deviceSentMessage?.message?.listMessage?.listType === + proto.Message.ListMessage.ListType.PRODUCT_LIST ) { message = JSON.parse(JSON.stringify(message)); - message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; + message.deviceSentMessage.message.listMessage.listType = + proto.Message.ListMessage.ListType.SINGLE_SELECT; } - if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) { + if ( + message.listMessage?.listType == + proto.Message.ListMessage.ListType.PRODUCT_LIST + ) { message = JSON.parse(JSON.stringify(message)); - message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; + message.listMessage.listType = + proto.Message.ListMessage.ListType.SINGLE_SELECT; } return message; @@ -680,8 +781,16 @@ 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); + if ( + this.localSettings.wavoipToken && + this.localSettings.wavoipToken.length > 0 + ) { + useVoiceCallsBaileys( + this.localSettings.wavoipToken, + this.client, + this.connectionStatus.state as any, + true + ); } this.eventHandler(); @@ -733,7 +842,9 @@ export class BaileysStartupService extends ChannelStartupService { select: { remoteJid: true }, }); - const existingChatIdSet = new Set(existingChatIds.map((chat) => chat.remoteJid)); + const existingChatIdSet = new Set( + existingChatIds.map((chat) => chat.remoteJid) + ); const chatsToInsert = chats .filter((chat) => !existingChatIdSet?.has(chat.id)) @@ -748,7 +859,10 @@ 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, + }); } }, @@ -757,7 +871,7 @@ export class BaileysStartupService extends ChannelStartupService { proto.IConversation & { lastMessageRecvTimestamp?: number } & { conditional: (bufferedData: BufferedEventData) => boolean; } - >[], + >[] ) => { const chatsRaw = chats.map((chat) => { return { remoteJid: chat.id, instanceId: this.instanceId }; @@ -767,7 +881,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 }, }); } @@ -776,7 +894,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]); @@ -788,7 +908,8 @@ export class BaileysStartupService extends ChannelStartupService { try { const contactsRaw: any = contacts.map((contact) => ({ remoteJid: contact.id, - pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], + pushName: + contact?.name || contact?.verifiedName || contact.id.split('@')[0], profilePicUrl: null, instanceId: this.instanceId, })); @@ -797,11 +918,18 @@ 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')); + const usersContacts = contactsRaw.filter((c) => + c.remoteJid.includes('@s.whatsapp') + ); if (usersContacts) { - await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid }))); + await saveOnWhatsappCache( + usersContacts.map((c) => ({ remoteJid: c.remoteJid })) + ); } } @@ -813,57 +941,79 @@ export class BaileysStartupService extends ChannelStartupService { ) { this.chatwootService.addHistoryContacts( { instanceName: this.instance.name, instanceId: this.instance.id }, - contactsRaw, + contactsRaw ); chatwootImport.importHistoryContacts( { instanceName: this.instance.name, instanceId: this.instance.id }, - this.localChatwoot, + this.localChatwoot ); } const updatedContacts = await Promise.all( contacts.map(async (contact) => ({ remoteJid: contact.id, - pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], - profilePicUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + pushName: + contact?.name || + contact?.verifiedName || + contact.id.split('@')[0], + profilePicUrl: ( + await this.profilePicture(contact.id) + ).profilePictureUrl, instanceId: this.instanceId, - })), + })) ); if (updatedContacts.length > 0) { - const usersContacts = updatedContacts.filter((c) => c.remoteJid.includes('@s.whatsapp')); + const usersContacts = updatedContacts.filter((c) => + c.remoteJid.includes('@s.whatsapp') + ); if (usersContacts) { - await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid }))); + await saveOnWhatsappCache( + usersContacts.map((c) => ({ remoteJid: c.remoteJid })) + ); } this.sendDataWebhook(Events.CONTACTS_UPDATE, updatedContacts); await Promise.all( updatedContacts.map(async (contact) => { const update = this.prismaRepository.contact.updateMany({ - where: { remoteJid: contact.remoteJid, instanceId: this.instanceId }, + 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 }; + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { + const instance = { + instanceName: this.instance.name, + instanceId: this.instance.id, + }; const findParticipant = await this.chatwootService.findContact( instance, - contact.remoteJid.split('@')[0], + contact.remoteJid.split('@')[0] ); if (!findParticipant) { return; } - this.chatwootService.updateContact(instance, findParticipant.id, { - name: contact.pushName, - avatar_url: contact.profilePicUrl, - }); + this.chatwootService.updateContact( + instance, + findParticipant.id, + { + name: contact.pushName, + avatar_url: contact.profilePicUrl, + } + ); } return update; - }), + }) ); } } catch (error) { @@ -873,13 +1023,21 @@ 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)}`); + this.logger.debug( + `Updating contact: ${JSON.stringify(contact, null, 2)}` + ); contactsRaw.push({ remoteJid: contact.id, pushName: contact?.name ?? contact?.verifiedName, - profilePicUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + profilePicUrl: (await this.profilePicture(contact.id)) + .profilePictureUrl, instanceId: this.instanceId, }); } @@ -888,10 +1046,15 @@ export class BaileysStartupService extends ChannelStartupService { const updateTransactions = contactsRaw.map((contact) => this.prismaRepository.contact.upsert({ - where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } }, + where: { + remoteJid_instanceId: { + remoteJid: contact.remoteJid, + instanceId: contact.instanceId, + }, + }, create: contact, update: contact, - }), + }) ); await this.prismaRepository.$transaction(updateTransactions); @@ -920,7 +1083,7 @@ export class BaileysStartupService extends ChannelStartupService { console.log('received on-demand history sync, messages=', messages); } console.log( - `recv ${chats.length} chats, ${contacts.length} contacts, ${messages.length} msgs (is latest: ${isLatest}, progress: ${progress}%), type: ${syncType}`, + `recv ${chats.length} chats, ${contacts.length} contacts, ${messages.length} msgs (is latest: ${isLatest}, progress: ${progress}%), type: ${syncType}` ); const instance: InstanceDto = { instanceName: this.instance.name }; @@ -928,12 +1091,19 @@ export class BaileysStartupService extends ChannelStartupService { let timestampLimitToImport = null; if (this.configService.get('CHATWOOT').ENABLED) { - const daysLimitToImport = this.localChatwoot?.enabled ? this.localChatwoot.daysLimitImportMessages : 1000; + const daysLimitToImport = this.localChatwoot?.enabled + ? this.localChatwoot.daysLimitImportMessages + : 1000; const date = new Date(); - timestampLimitToImport = new Date(date.setDate(date.getDate() - daysLimitToImport)).getTime() / 1000; + timestampLimitToImport = + new Date( + date.setDate(date.getDate() - daysLimitToImport) + ).getTime() / 1000; - const maxBatchTimestamp = Math.max(...messages.map((message) => message.messageTimestamp as number)); + const maxBatchTimestamp = Math.max( + ...messages.map((message) => message.messageTimestamp as number) + ); const processBatch = maxBatchTimestamp >= timestampLimitToImport; @@ -946,15 +1116,24 @@ export class BaileysStartupService extends ChannelStartupService { for (const contact of contacts) { if (contact.id && (contact.notify || contact.name)) { - contactsMap.set(contact.id, { name: contact.name ?? contact.notify, jid: contact.id }); + contactsMap.set(contact.id, { + name: contact.name ?? contact.notify, + jid: contact.id, + }); } } - const chatsRaw: { remoteJid: string; instanceId: string; name?: string }[] = []; + 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) { @@ -962,33 +1141,43 @@ 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( chatwootImport.getRepositoryMessagesCache(instance) ?? - ( - await this.prismaRepository.message.findMany({ - select: { key: true }, - where: { instanceId: this.instanceId }, - }) - ).map((message) => { - const key = message.key as { id: string }; + ( + await this.prismaRepository.message.findMany({ + select: { key: true }, + where: { instanceId: this.instanceId }, + }) + ).map((message) => { + const key = message.key as { id: string }; - return key.id; - }), + return key.id; + }) ); if (chatwootImport.getRepositoryMessagesCache(instance) === null) { - chatwootImport.setRepositoryMessagesCache(instance, messagesRepository); + chatwootImport.setRepositoryMessagesCache( + instance, + messagesRepository + ); } for (const m of messages) { @@ -1011,7 +1200,8 @@ export class BaileysStartupService extends ChannelStartupService { } if (!m.pushName && !m.key.fromMe) { - const participantJid = m.participant || m.key.participant || m.key.remoteJid; + const participantJid = + m.participant || m.key.participant || m.key.remoteJid; if (participantJid && contactsMap.has(participantJid)) { m.pushName = contactsMap.get(participantJid).name; } else if (participantJid) { @@ -1025,7 +1215,10 @@ export class BaileysStartupService extends ChannelStartupService { 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 ( @@ -1036,12 +1229,16 @@ export class BaileysStartupService extends ChannelStartupService { ) { this.chatwootService.addHistoryMessages( instance, - messagesRaw.filter((msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid)), + messagesRaw.filter( + (msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid) + ) ); } 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; @@ -1053,8 +1250,12 @@ export class BaileysStartupService extends ChannelStartupService { }, 'messages.upsert': async ( - { messages, type, requestId }: { messages: WAMessage[]; type: MessageUpsertType; requestId?: string }, - settings: any, + { + messages, + type, + requestId, + }: { messages: WAMessage[]; type: MessageUpsertType; requestId?: string }, + settings: any ) => { try { for (const received of messages) { @@ -1068,97 +1269,252 @@ export class BaileysStartupService extends ChannelStartupService { 'Invalid PreKey ID', 'No session record', 'No session found to decrypt message', - ].some((err) => param?.includes?.(err)), + ].some((err) => param?.includes?.(err)) ) ) { - this.logger.warn(`Message ignored with messageStubParameters: ${JSON.stringify(received, null, 2)}`); + 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; + if ( + received.message?.conversation || + received.message?.extendedTextMessage?.text + ) { + const text = + received.message?.conversation || + received.message?.extendedTextMessage?.text; if (text == 'requestPlaceholder' && !requestId) { - const messageId = await this.client.requestPlaceholderResend(received.key); + const messageId = await this.client.requestPlaceholderResend( + received.key + ); console.log('requested placeholder resync, id=', messageId); } else if (requestId) { - console.log('Message received from phone, id=', requestId, received); + console.log( + 'Message received from phone, id=', + requestId, + received + ); } if (text == 'onDemandHistSync') { - const messageId = await this.client.fetchMessageHistory(50, received.key, received.messageTimestamp!); + const messageId = await this.client.fetchMessageHistory( + 50, + received.key, + received.messageTimestamp! + ); console.log('requested on-demand sync, id=', messageId); } } + // EDIT/DELETE (Baileys) sem duplicação e sem texto no WhatsApp + const protocolMsg: any = + received?.message?.protocolMessage || + received?.message?.editedMessage?.message?.protocolMessage; + + const isStubRevoke = + received?.messageStubType === proto.WebMessageInfo.StubType.REVOKE; + + // DELETE (somente via StubType) + if (isStubRevoke) { + const keyToSearch: any = received?.key ?? {}; + const status = 'DELETED'; + const webhookEvent = Events.MESSAGES_DELETE; + + // anti-dup (absorve REVOKE múltiplo: stub + outras origens) + const delKey = `cw_del_${this.instance.id}_${keyToSearch?.id}`; + const seen = await this.baileysCache.get(delKey); + if (seen) { + this.logger.info(`Skip duplicate delete for ${keyToSearch?.id}`); + continue; + } + await this.baileysCache.set(delKey, true, 15); + + // payload limpo para DB; WhatsApp NÃO recebe placeholder + const messageContentUpdate = { conversation: '' }; + + // Chatwoot: manda o placeholder + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { + const chatwootPayload: any = { + ...received, + key: keyToSearch, + text: '🗑️ O remetente apagou uma mensagem.', + }; + + await this.chatwootService.eventWhatsapp( + Events.MESSAGES_DELETE, + { instanceName: this.instance.name, instanceId: this.instance.id }, + chatwootPayload + ); + } - const editedMessage = - received?.message?.protocolMessage || received?.message?.editedMessage?.message?.protocolMessage; + // Webhook externo + await this.sendDataWebhook(webhookEvent, received); - if (editedMessage) { - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) - this.chatwootService.eventWhatsapp( + // Persistência local + const oldMessage = await this.getMessage(keyToSearch, true); + if ((oldMessage as any)?.id) { + const editedMessageTimestamp = Long.isLong(received?.messageTimestamp) + ? Math.floor((received?.messageTimestamp as Long).toNumber()) + : Math.floor((received?.messageTimestamp as number) ?? Date.now() / 1000); + + await this.prismaRepository.message.update({ + where: { id: (oldMessage as any).id }, + data: { + message: messageContentUpdate, + messageTimestamp: editedMessageTimestamp, + status, + }, + }); + + await this.prismaRepository.messageUpdate.create({ + data: { + fromMe: keyToSearch?.fromMe ?? false, + keyId: keyToSearch?.id ?? '', + remoteJid: keyToSearch?.remoteJid ?? '', + status, + instanceId: this.instanceId, + messageId: (oldMessage as any).id, + }, + }); + } + + continue; // não cai no MESSAGES_UPSERT + } + + // EDIT (somente quando NÃO for REVOKE) + if (protocolMsg && protocolMsg.type !== proto.Message.ProtocolMessage.Type.REVOKE) { + const payloadToProcess: any = protocolMsg; + const keyToSearch: any = payloadToProcess?.key ?? received?.key; + + // conteúdo de edição (WhatsApp não recebe placeholder) + const messageContentUpdate: any = + (payloadToProcess as any)?.editedMessage || { conversation: '' }; + const status = 'EDITED'; + const webhookEvent = Events.MESSAGES_EDITED; + + // Chatwoot: envia texto editado + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { + const chatwootPayload: any = { + ...payloadToProcess, + key: keyToSearch, + editedMessage: + (payloadToProcess as any)?.editedMessage ?? messageContentUpdate, + }; + + const friendlyText = + messageContentUpdate?.conversation ?? + messageContentUpdate?.extendedTextMessage?.text ?? + messageContentUpdate?.imageMessage?.caption ?? + messageContentUpdate?.videoMessage?.caption ?? + messageContentUpdate?.documentMessage?.caption ?? + (payloadToProcess as any)?.text ?? + ''; + + if (friendlyText) { + chatwootPayload.text = friendlyText; + } + + await this.chatwootService.eventWhatsapp( 'messages.edit', { instanceName: this.instance.name, instanceId: this.instance.id }, - editedMessage, + chatwootPayload ); + } - await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage); - const oldMessage = await this.getMessage(editedMessage.key, true); + // Webhook externo + await this.sendDataWebhook(webhookEvent, payloadToProcess); + + // Persistência local + const oldMessage = await this.getMessage(keyToSearch, true); if ((oldMessage as any)?.id) { const editedMessageTimestamp = Long.isLong(received?.messageTimestamp) - ? Math.floor(received?.messageTimestamp.toNumber()) - : Math.floor(received?.messageTimestamp as number); + ? Math.floor((received?.messageTimestamp as Long).toNumber()) + : Math.floor((received?.messageTimestamp as number) ?? Date.now() / 1000); await this.prismaRepository.message.update({ where: { id: (oldMessage as any).id }, data: { - message: editedMessage.editedMessage as any, + message: messageContentUpdate, messageTimestamp: editedMessageTimestamp, - status: 'EDITED', + status, }, }); + await this.prismaRepository.messageUpdate.create({ data: { - fromMe: editedMessage.key.fromMe, - keyId: editedMessage.key.id, - remoteJid: editedMessage.key.remoteJid, - status: 'EDITED', + fromMe: keyToSearch?.fromMe ?? false, + keyId: keyToSearch?.id ?? '', + remoteJid: keyToSearch?.remoteJid ?? '', + status, instanceId: this.instanceId, messageId: (oldMessage as any).id, }, }); } + + continue; // não cai no MESSAGES_UPSERT } + // FIM EDIT/DELETE (Baileys) + // flag local só pra deduplicar nesse bloco normal de upsert + const isEditOrDelete = !!protocolMsg || (received?.messageStubType === proto.WebMessageInfo.StubType.REVOKE); const messageKey = `${this.instance.id}_${received.key.id}`; const cached = await this.baileysCache.get(messageKey); - if (cached && !editedMessage && !requestId) { + // antes: if (cached && !editedMessage && !requestId) { + if (cached && !isEditOrDelete && !requestId) { this.logger.info(`Message duplicated ignored: ${received.key.id}`); continue; } - - await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS); - + //await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS); comentei aqui by rafael if ( (type !== 'notify' && type !== 'append') || - editedMessage || + // antes: editedMessage || + isEditOrDelete || received.message?.pollUpdateMessage || !received?.message ) { continue; } + // lock otimista contra duplicação na 1ª mensagem da conversa + const lockKey = `lock_${messageKey}`; + const hasLock = await this.baileysCache.get(lockKey); + if (hasLock) { + this.logger.info(`(lock) duplicated start ignored: ${received.key.id}`); + continue; + } + // marca lock com TTL curto (15–20s é suficiente) + await this.baileysCache.set(lockKey, true, 20); + if (Long.isLong(received.messageTimestamp)) { received.messageTimestamp = received.messageTimestamp?.toNumber(); } - if (settings?.groupsIgnore && received.key.remoteJid.includes('@g.us')) { + 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 }, + where: { + instanceId: this.instanceId, + remoteJid: received.key.remoteJid, + }, select: { id: true, name: true }, }); @@ -1170,7 +1526,9 @@ export class BaileysStartupService extends ChannelStartupService { !received.key.fromMe && !received.key.remoteJid.includes('@g.us') ) { - this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]); + this.sendDataWebhook(Events.CHATS_UPSERT, [ + { ...existingChat, name: received.pushName }, + ]); if (this.configService.get('DATABASE').SAVE_DATA.CHATS) { try { await this.prismaRepository.chat.update({ @@ -1178,7 +1536,9 @@ export class BaileysStartupService extends ChannelStartupService { data: { name: received.pushName }, }); } catch { - console.log(`Chat insert record ignored: ${received.key.remoteJid} - ${this.instanceId}`); + console.log( + `Chat insert record ignored: ${received.key.remoteJid} - ${this.instanceId}` + ); } } } @@ -1196,11 +1556,17 @@ export class BaileysStartupService extends ChannelStartupService { const isVideo = received?.message?.videoMessage; - if (this.localSettings.readMessages && received.key.id !== 'status@broadcast') { + if ( + this.localSettings.readMessages && + received.key.id !== 'status@broadcast' + ) { await this.client.readMessages([received.key]); } - if (this.localSettings.readStatus && received.key.id === 'status@broadcast') { + if ( + this.localSettings.readStatus && + received.key.id === 'status@broadcast' + ) { await this.client.readMessages([received.key]); } @@ -1209,32 +1575,67 @@ export class BaileysStartupService extends ChannelStartupService { this.localChatwoot?.enabled && !received.key.id.includes('@broadcast') ) { - const chatwootSentMessage = await this.chatwootService.eventWhatsapp( - Events.MESSAGES_UPSERT, - { instanceName: this.instance.name, instanceId: this.instanceId }, - messageRaw, - ); + const chatwootSentMessage = + await this.chatwootService.eventWhatsapp( + Events.MESSAGES_UPSERT, + { + instanceName: this.instance.name, + instanceId: this.instanceId, + }, + messageRaw + ); if (chatwootSentMessage?.id) { messageRaw.chatwootMessageId = chatwootSentMessage.id; messageRaw.chatwootInboxId = chatwootSentMessage.inbox_id; - messageRaw.chatwootConversationId = chatwootSentMessage.conversation_id; + messageRaw.chatwootConversationId = + chatwootSentMessage.conversation_id; + + // evita duplicação após o Chatwoot responder + await this.baileysCache.set( + messageKey, + true, + this.MESSAGE_CACHE_TTL_SECONDS + ); } + + } else { + // Chatwoot desativado → ainda assim evita duplicação + await this.baileysCache.set( + messageKey, + true, + this.MESSAGE_CACHE_TTL_SECONDS + ); } - if (this.configService.get('OPENAI').ENABLED && received?.message?.audioMessage) { - const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ - where: { instanceId: this.instanceId }, - include: { OpenaiCreds: true }, - }); + if ( + this.configService.get('OPENAI').ENABLED && + received?.message?.audioMessage + ) { + const openAiDefaultSettings = + await this.prismaRepository.openaiSetting.findFirst({ + where: { instanceId: this.instanceId }, + include: { OpenaiCreds: true }, + }); - if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { - messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`; + if ( + openAiDefaultSettings && + openAiDefaultSettings.openaiCredsId && + openAiDefaultSettings.speechToText + ) { + messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText( + received, + this + )}`; } } - if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { - const msg = await this.prismaRepository.message.create({ data: messageRaw }); + if ( + this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE + ) { + const msg = await this.prismaRepository.message.create({ + data: messageRaw, + }); const { remoteJid } = received.key; const timestamp = msg.messageTimestamp; @@ -1249,25 +1650,43 @@ export class BaileysStartupService extends ChannelStartupService { 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); + 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); + this.logger.log( + `Update readed messages ${remoteJid} - ${timestamp}` + ); + await this.updateMessagesReadedByTimestamp( + remoteJid, + timestamp + ); } - await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS); + await this.baileysCache.set( + messageKey, + true, + this.MESSAGE_CACHE_TTL_SECONDS + ); } else { - this.logger.info(`Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}`); + this.logger.info( + `Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}` + ); } 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.'); + this.logger.warn( + 'Video upload is disabled. Skipping video upload.' + ); // Skip video upload by returning early from this block return; } @@ -1278,9 +1697,14 @@ export class BaileysStartupService extends ChannelStartupService { const hasRealMedia = this.hasValidMediaContent(message); if (!hasRealMedia) { - this.logger.warn('Message detected as media but contains no valid media content'); + this.logger.warn( + 'Message detected as media but contains no valid media content' + ); } else { - const media = await this.getBase64FromMediaMessage({ message }, true); + const media = await this.getBase64FromMediaMessage( + { message }, + true + ); const { buffer, mediaType, fileName, size } = media; const mimetype = mimeTypes.lookup(fileName).toString(); @@ -1288,9 +1712,14 @@ export class BaileysStartupService extends ChannelStartupService { `${this.instance.id}`, received.key.remoteJid, mediaType, - `${Date.now()}_${fileName}`, + `${Date.now()}_${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: { @@ -1306,10 +1735,17 @@ export class BaileysStartupService extends ChannelStartupService { 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]); + this.logger.error([ + 'Error on upload file to minio', + error?.message, + error?.stack, + ]); } } } @@ -1322,7 +1758,10 @@ export class BaileysStartupService extends ChannelStartupService { { key: received.key, message: received?.message }, 'buffer', {}, - { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + } ); if (buffer) { @@ -1333,7 +1772,10 @@ export class BaileysStartupService extends ChannelStartupService { { key: received.key, message: received?.message }, 'buffer', {}, - { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + } ); if (buffer) { @@ -1341,26 +1783,37 @@ export class BaileysStartupService extends ChannelStartupService { } } } catch (error) { - this.logger.error(['Error converting media to base64', error?.message]); + this.logger.error([ + 'Error converting media to base64', + error?.message, + ]); } } } this.logger.verbose(messageRaw); - sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); + sendTelemetry( + `received.message.${messageRaw.messageType ?? 'unknown'}` + ); this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); await chatbotController.emit({ - instance: { instanceName: this.instance.name, instanceId: this.instanceId }, + instance: { + instanceName: this.instance.name, + instanceId: this.instanceId, + }, remoteJid: messageRaw.key.remoteJid, msg: messageRaw, pushName: messageRaw.pushName, }); const contact = await this.prismaRepository.contact.findFirst({ - where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId }, + where: { + remoteJid: received.key.remoteJid, + instanceId: this.instanceId, + }, }); const contactRaw: { @@ -1370,8 +1823,13 @@ export class BaileysStartupService extends ChannelStartupService { instanceId: string; } = { remoteJid: received.key.remoteJid, - pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName, - profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + pushName: received.key.fromMe + ? '' + : received.key.fromMe == null + ? '' + : received.pushName, + profilePicUrl: (await this.profilePicture(received.key.remoteJid)) + .profilePictureUrl, instanceId: this.instanceId, }; @@ -1379,11 +1837,16 @@ export class BaileysStartupService extends ChannelStartupService { continue; } - if (contactRaw.remoteJid.includes('@s.whatsapp') || contactRaw.remoteJid.includes('@lid')) { + if ( + contactRaw.remoteJid.includes('@s.whatsapp') || + contactRaw.remoteJid.includes('@lid') + ) { await saveOnWhatsappCache([ { remoteJid: - messageRaw.key.addressingMode === 'lid' ? messageRaw.key.remoteJidAlt : messageRaw.key.remoteJid, + messageRaw.key.addressingMode === 'lid' + ? messageRaw.key.remoteJidAlt + : messageRaw.key.remoteJid, remoteJidAlt: messageRaw.key.remoteJidAlt, lid: messageRaw.key.addressingMode === 'lid' ? 'lid' : null, }, @@ -1393,17 +1856,28 @@ export class BaileysStartupService extends ChannelStartupService { if (contact) { this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { await this.chatwootService.eventWhatsapp( Events.CONTACTS_UPDATE, - { instanceName: this.instance.name, instanceId: this.instanceId }, - contactRaw, + { + instanceName: this.instance.name, + instanceId: this.instanceId, + }, + contactRaw ); } 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, + }, + }, create: contactRaw, update: contactRaw, }); @@ -1415,7 +1889,12 @@ 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, }); @@ -1425,8 +1904,13 @@ export class BaileysStartupService extends ChannelStartupService { } }, - 'messages.update': async (args: { update: Partial; key: WAMessageKey }[], settings: any) => { - this.logger.verbose(`Update messages ${JSON.stringify(args, undefined, 2)}`); + 'messages.update': async ( + args: { update: Partial; key: WAMessageKey }[], + settings: any + ) => { + this.logger.verbose( + `Update messages ${JSON.stringify(args, undefined, 2)}` + ); const readChatToUpdate: Record = {}; // {remoteJid: true} @@ -1442,18 +1926,23 @@ export class BaileysStartupService extends ChannelStartupService { const cached = await this.baileysCache.get(updateKey); if (cached) { - this.logger.info(`Message duplicated ignored [avoid deadlock]: ${updateKey}`); + this.logger.info( + `Message duplicated ignored [avoid deadlock]: ${updateKey}` + ); continue; } await this.baileysCache.set(updateKey, true, 30 * 60); if (status[update.status] === 'READ' && key.fromMe) { - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { this.chatwootService.eventWhatsapp( 'messages.read', { instanceName: this.instance.name, instanceId: this.instanceId }, - { key: key }, + { key: key } ); } } @@ -1483,19 +1972,24 @@ export class BaileysStartupService extends ChannelStartupService { }; let findMessage: any; - const configDatabaseData = this.configService.get('DATABASE').SAVE_DATA; + const configDatabaseData = + this.configService.get('DATABASE').SAVE_DATA; if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) { // Use raw SQL to avoid JSON path issues const messages = (await this.prismaRepository.$queryRaw` - SELECT * FROM "Message" - WHERE "instanceId" = ${this.instanceId} - AND "key"->>'id' = ${key.id} + SELECT * FROM 'Message' + WHERE 'instanceId' = ${this.instanceId} + AND 'key'->>'id' = ${key.id} 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)}`); + this.logger.warn( + `Original message not found for update. Skipping. Key: ${JSON.stringify( + key + )}` + ); continue; } message.messageId = findMessage.id; @@ -1504,21 +1998,36 @@ export class BaileysStartupService extends ChannelStartupService { if (update.message === null && update.status === undefined) { this.sendDataWebhook(Events.MESSAGES_DELETE, key); - if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) - await this.prismaRepository.messageUpdate.create({ data: message }); + if ( + this.configService.get('DATABASE').SAVE_DATA + .MESSAGE_UPDATE + ) + await this.prismaRepository.messageUpdate.create({ + data: message, + }); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { this.chatwootService.eventWhatsapp( Events.MESSAGES_DELETE, - { instanceName: this.instance.name, instanceId: this.instanceId }, - { key: key }, + { + instanceName: this.instance.name, + instanceId: this.instanceId, + }, + { key: key } ); } continue; } - if (findMessage && update.status !== undefined && status[update.status] !== findMessage.status) { + if ( + findMessage && + update.status !== undefined && + status[update.status] !== findMessage.status + ) { if (!key.fromMe && key.remoteJid) { readChatToUpdate[key.remoteJid] = true; @@ -1531,9 +2040,18 @@ export class BaileysStartupService extends ChannelStartupService { 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); + 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({ @@ -1542,7 +2060,7 @@ export class BaileysStartupService extends ChannelStartupService { }); } else { this.logger.info( - `Update readed messages duplicated ignored in message.update [avoid deadlock]: ${messageKey}`, + `Update readed messages duplicated ignored in message.update [avoid deadlock]: ${messageKey}` ); } } @@ -1550,29 +2068,48 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.MESSAGES_UPDATE, message); - if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) + 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 }, + 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, + 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 }); + await this.prismaRepository.chat.update({ + where: { id: existingChat.id }, + data: chatToInsert, + }); } catch { - console.log(`Chat insert record ignored: ${chatToInsert.remoteJid} - ${chatToInsert.instanceId}`); + console.log( + `Chat insert record ignored: ${chatToInsert.remoteJid} - ${chatToInsert.instanceId}` + ); } } } } } - await Promise.all(Object.keys(readChatToUpdate).map((remoteJid) => this.updateChatUnreadMessages(remoteJid))); + await Promise.all( + Object.keys(readChatToUpdate).map((remoteJid) => + this.updateChatUnreadMessages(remoteJid) + ) + ); }, }; @@ -1609,31 +2146,42 @@ export class BaileysStartupService extends ChannelStartupService { try { // Usa o mesmo método que o endpoint /group/participants - const groupParticipants = await this.findParticipants({ groupJid: participantsUpdate.id }); + 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'); + 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); + 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); - } + let phoneNumber: string; + if (participantData?.phoneNumber) { + phoneNumber = participantData.phoneNumber; + } else { + phoneNumber = normalizePhoneNumber(participantId); + } - return { - jid: participantId, - phoneNumber, - name: participantData?.name, - imgUrl: participantData?.imgUrl, - }; - }); + return { + jid: participantId, + phoneNumber, + name: participantData?.name, + imgUrl: participantData?.imgUrl, + }; + } + ); // Mantém formato original + adiciona dados resolvidos const enhancedParticipantsUpdate = { @@ -1643,13 +2191,19 @@ export class BaileysStartupService extends ChannelStartupService { participantsData: resolvedParticipants, }; - this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, enhancedParticipantsUpdate); + 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}`, + `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); @@ -1658,21 +2212,38 @@ export class BaileysStartupService extends ChannelStartupService { private readonly labelHandle = { [Events.LABELS_EDIT]: async (label: Label) => { - this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name }); + 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) { await this.prismaRepository.label.delete({ - where: { labelId_instanceId: { instanceId: this.instanceId, labelId: label.id } }, + where: { + labelId_instanceId: { + instanceId: this.instanceId, + labelId: label.id, + }, + }, + }); + this.sendDataWebhook(Events.LABELS_EDIT, { + ...label, + instance: this.instance.name, }); - this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name }); return; } const labelName = label.name.replace(/[^\x20-\x7E]/g, ''); - if (!savedLabel || savedLabel.color !== `${label.color}` || savedLabel.name !== labelName) { + if ( + !savedLabel || + savedLabel.color !== `${label.color}` || + savedLabel.name !== labelName + ) { if (this.configService.get('DATABASE').SAVE_DATA.LABELS) { const labelData = { color: `${label.color}`, @@ -1682,7 +2253,12 @@ 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, }); @@ -1692,10 +2268,10 @@ export class BaileysStartupService extends ChannelStartupService { [Events.LABELS_ASSOCIATION]: async ( data: { association: LabelAssociation; type: 'remove' | 'add' }, - database: Database, + database: Database ) => { this.logger.info( - `labels association - ${data?.association?.chatId} (${data.type}-${data?.association?.type}): ${data?.association?.labelId}`, + `labels association - ${data?.association?.chatId} (${data.type}-${data?.association?.type}): ${data?.association?.labelId}` ); if (database.SAVE_DATA.CHATS) { const instanceId = this.instanceId; @@ -1733,11 +2309,19 @@ export class BaileysStartupService extends ChannelStartupService { 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); + call.from = + await this.client.signalRepository.lidMapping.getPNForLID( + call.from as string + ); } - const msg = await this.client.sendMessage(call.from, { text: settings.msgCall }); + const msg = await this.client.sendMessage(call.from, { + text: settings.msgCall, + }); - this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify' }); + this.client.ev.emit('messages.upsert', { + messages: [msg], + type: 'notify', + }); } this.sendDataWebhook(Events.CALL, call); @@ -1769,19 +2353,27 @@ export class BaileysStartupService extends ChannelStartupService { } if (events['message-receipt.update']) { - const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[]; + const payload = events[ + 'message-receipt.update' + ] as MessageUserReceiptUpdate[]; const remotesJidMap: Record = {}; for (const event of payload) { - if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') { + if ( + typeof event.key.remoteJid === 'string' && + typeof event.receipt.readTimestamp === 'number' + ) { remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp; } } await Promise.all( Object.keys(remotesJidMap).map(async (remoteJid) => - this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]), - ), + this.updateMessagesReadedByTimestamp( + remoteJid, + remotesJidMap[remoteJid] + ) + ) ); } @@ -1875,7 +2467,9 @@ export class BaileysStartupService extends ChannelStartupService { return true; } - private isSyncNotificationFromUsedSyncType(msg: proto.Message.IHistorySyncNotification) { + private isSyncNotificationFromUsedSyncType( + msg: proto.Message.IHistorySyncNotification + ) { return ( (this.localSettings.syncFullHistory && msg?.syncType === 2) || (!this.localSettings.syncFullHistory && msg?.syncType === 3) @@ -1886,7 +2480,10 @@ export class BaileysStartupService extends ChannelStartupService { const jid = createJid(number); try { - const profilePictureUrl = await this.client.profilePictureUrl(jid, 'image'); + const profilePictureUrl = await this.client.profilePictureUrl( + jid, + 'image' + ); return { wuid: jid, profilePictureUrl }; } catch { @@ -1898,7 +2495,10 @@ export class BaileysStartupService extends ChannelStartupService { const jid = createJid(number); try { - return { wuid: jid, status: (await this.client.fetchStatus(jid))[0]?.status }; + return { + wuid: jid, + status: (await this.client.fetchStatus(jid))[0]?.status, + }; } catch { return { wuid: jid, status: null }; } @@ -1949,7 +2549,14 @@ export class BaileysStartupService extends ChannelStartupService { }; } } catch { - return { wuid: jid, name: null, picture: null, status: null, os: null, isBusiness: false }; + return { + wuid: jid, + name: null, + picture: null, + status: null, + os: null, + isBusiness: false, + }; } } @@ -1975,7 +2582,7 @@ export class BaileysStartupService extends ChannelStartupService { quoted: any, messageId?: string, ephemeralExpiration?: number, - contextInfo?: any, + contextInfo?: any // participants?: GroupParticipant[], ) { sender = sender.toLowerCase(); @@ -2003,7 +2610,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: isPnUser(sender) ? sender : undefined, + fromMe: true, + }; for (const [key, value] of Object.entries(m)) { if (!value || (isArray(value) && value.length) === 0) { delete m[key]; @@ -2023,9 +2635,12 @@ 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, + option as unknown as MiscMessageGenerationOptions ); } } @@ -2043,19 +2658,27 @@ export class BaileysStartupService extends ChannelStartupService { linkPreview: linkPreview, contextInfo: message['contextInfo'], } as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, + option as unknown as MiscMessageGenerationOptions ); } - if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') { + if ( + !message['audio'] && + !message['poll'] && + !message['sticker'] && + sender != 'status@broadcast' + ) { 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, + option as unknown as MiscMessageGenerationOptions ); } @@ -2063,7 +2686,10 @@ 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); @@ -2073,8 +2699,9 @@ export class BaileysStartupService extends ChannelStartupService { const batchSize = 10; - const batches = Array.from({ length: Math.ceil(jidList.length / batchSize) }, (_, i) => - jidList.slice(i * batchSize, i * batchSize + batchSize), + const batches = Array.from( + { length: Math.ceil(jidList.length / batchSize) }, + (_, i) => jidList.slice(i * batchSize, i * batchSize + batchSize) ); let msgId: string | null = null; @@ -2091,7 +2718,7 @@ export class BaileysStartupService extends ChannelStartupService { backgroundColor: message['status'].option.backgroundColor, font: message['status'].option.font, statusJidList: firstBatch, - } as unknown as MiscMessageGenerationOptions, + } as unknown as MiscMessageGenerationOptions ); msgId = firstMessage.key.id; @@ -2109,11 +2736,11 @@ export class BaileysStartupService extends ChannelStartupService { font: message['status'].option.font, statusJidList: batch, messageId: msgId, - } as unknown as MiscMessageGenerationOptions, + } as unknown as MiscMessageGenerationOptions ); return messageSent; - }), + }) ); return firstMessage; @@ -2122,7 +2749,7 @@ export class BaileysStartupService extends ChannelStartupService { return await this.client.sendMessage( sender, message as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, + option as unknown as MiscMessageGenerationOptions ); } @@ -2130,11 +2757,15 @@ export class BaileysStartupService extends ChannelStartupService { number: string, message: T, options?: Options, - isIntegration = false, + isIntegration = false ) { const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); - if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + if ( + !isWA.exists && + !isJidGroup(isWA.jid) && + !isWA.jid.includes('@broadcast') + ) { throw new BadRequestException(isWA); } @@ -2150,7 +2781,10 @@ export class BaileysStartupService extends ChannelStartupService { while (remainingDelay > 20000) { await this.client.presenceSubscribe(sender); - await this.client.sendPresenceUpdate((options.presence as WAPresence) ?? 'composing', sender); + await this.client.sendPresenceUpdate( + (options.presence as WAPresence) ?? 'composing', + sender + ); await delay(20000); @@ -2161,7 +2795,10 @@ export class BaileysStartupService extends ChannelStartupService { if (remainingDelay > 0) { await this.client.presenceSubscribe(sender); - await this.client.sendPresenceUpdate((options.presence as WAPresence) ?? 'composing', sender); + await this.client.sendPresenceUpdate( + (options.presence as WAPresence) ?? 'composing', + sender + ); await delay(remainingDelay); @@ -2170,7 +2807,10 @@ export class BaileysStartupService extends ChannelStartupService { } else { await this.client.presenceSubscribe(sender); - await this.client.sendPresenceUpdate((options.presence as WAPresence) ?? 'composing', sender); + await this.client.sendPresenceUpdate( + (options.presence as WAPresence) ?? 'composing', + sender + ); await delay(options.delay); @@ -2185,7 +2825,9 @@ 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 WAMessage); if (msg) { quoted = msg; @@ -2201,7 +2843,8 @@ export class BaileysStartupService extends ChannelStartupService { let group; try { const cache = this.configService.get('CACHE'); - if (!cache.REDIS.ENABLED && !cache.LOCAL.ENABLED) group = await this.findGroup({ groupJid: sender }, 'inner'); + 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 { @@ -2231,7 +2874,7 @@ export class BaileysStartupService extends ChannelStartupService { linkPreview, quoted, null, - group?.ephemeralDuration, + group?.ephemeralDuration // group?.participants, ); } else { @@ -2254,7 +2897,7 @@ export class BaileysStartupService extends ChannelStartupService { quoted, null, undefined, - contextInfo, + contextInfo ); } @@ -2276,27 +2919,44 @@ export class BaileysStartupService extends ChannelStartupService { const isVideo = messageSent?.message?.videoMessage; - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !isIntegration) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled && + !isIntegration + ) { this.chatwootService.eventWhatsapp( Events.SEND_MESSAGE, { instanceName: this.instance.name, instanceId: this.instanceId }, - messageRaw, + messageRaw ); } - if (this.configService.get('OPENAI').ENABLED && messageRaw?.message?.audioMessage) { - const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ - where: { instanceId: this.instanceId }, - include: { OpenaiCreds: true }, - }); + if ( + this.configService.get('OPENAI').ENABLED && + messageRaw?.message?.audioMessage + ) { + const openAiDefaultSettings = + await this.prismaRepository.openaiSetting.findFirst({ + where: { instanceId: this.instanceId }, + include: { OpenaiCreds: true }, + }); - if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { - messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(messageRaw, this)}`; + if ( + openAiDefaultSettings && + openAiDefaultSettings.openaiCredsId && + openAiDefaultSettings.speechToText + ) { + messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText( + messageRaw, + this + )}`; } } 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 { @@ -2310,9 +2970,14 @@ export class BaileysStartupService extends ChannelStartupService { const hasRealMedia = this.hasValidMediaContent(message); if (!hasRealMedia) { - this.logger.warn('Message detected as media but contains no valid media content'); + this.logger.warn( + 'Message detected as media but contains no valid media content' + ); } else { - const media = await this.getBase64FromMediaMessage({ message }, true); + const media = await this.getBase64FromMediaMessage( + { message }, + true + ); const { buffer, mediaType, fileName, size } = media; @@ -2323,23 +2988,41 @@ export class BaileysStartupService extends ChannelStartupService { messageRaw.key.remoteJid, `${messageRaw.key.id}`, mediaType, - fileName, + 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 }, + 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 }); + 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]); + this.logger.error([ + 'Error on upload file to minio', + error?.message, + error?.stack, + ]); } } } @@ -2351,7 +3034,10 @@ export class BaileysStartupService extends ChannelStartupService { { key: messageRaw.key, message: messageRaw?.message }, 'buffer', {}, - { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + } ); if (buffer) { @@ -2362,7 +3048,10 @@ export class BaileysStartupService extends ChannelStartupService { { key: messageRaw.key, message: messageRaw?.message }, 'buffer', {}, - { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + } ); if (buffer) { @@ -2370,7 +3059,10 @@ export class BaileysStartupService extends ChannelStartupService { } } } catch (error) { - this.logger.error(['Error converting media to base64', error?.message]); + this.logger.error([ + 'Error converting media to base64', + error?.message, + ]); } } } @@ -2379,9 +3071,16 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && isIntegration) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled && + isIntegration + ) { await chatbotController.emit({ - instance: { instanceName: this.instance.name, instanceId: this.instanceId }, + instance: { + instanceName: this.instance.name, + instanceId: this.instanceId, + }, remoteJid: messageRaw.key.remoteJid, msg: messageRaw, pushName: messageRaw.pushName, @@ -2403,7 +3102,11 @@ export class BaileysStartupService extends ChannelStartupService { const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); - if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + if ( + !isWA.exists && + !isJidGroup(isWA.jid) && + !isWA.jid.includes('@broadcast') + ) { throw new BadRequestException(isWA); } @@ -2414,7 +3117,10 @@ export class BaileysStartupService extends ChannelStartupService { while (remainingDelay > 20000) { await this.client.presenceSubscribe(sender); - await this.client.sendPresenceUpdate((data?.presence as WAPresence) ?? 'composing', sender); + await this.client.sendPresenceUpdate( + (data?.presence as WAPresence) ?? 'composing', + sender + ); await delay(20000); @@ -2425,7 +3131,10 @@ export class BaileysStartupService extends ChannelStartupService { if (remainingDelay > 0) { await this.client.presenceSubscribe(sender); - await this.client.sendPresenceUpdate((data?.presence as WAPresence) ?? 'composing', sender); + await this.client.sendPresenceUpdate( + (data?.presence as WAPresence) ?? 'composing', + sender + ); await delay(remainingDelay); @@ -2434,7 +3143,10 @@ export class BaileysStartupService extends ChannelStartupService { } else { await this.client.presenceSubscribe(sender); - await this.client.sendPresenceUpdate((data?.presence as WAPresence) ?? 'composing', sender); + await this.client.sendPresenceUpdate( + (data?.presence as WAPresence) ?? 'composing', + sender + ); await delay(data?.delay); @@ -2479,14 +3191,20 @@ export class BaileysStartupService extends ChannelStartupService { mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, }, - isIntegration, + isIntegration ); } 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', @@ -2494,7 +3212,7 @@ export class BaileysStartupService extends ChannelStartupService { linkPreview: data?.linkPreview, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, - }, + } ); } @@ -2508,13 +3226,17 @@ 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'); } - status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.remoteJid); + status.statusJidList = contacts + .filter((contact) => contact.pushName) + .map((contact) => contact.remoteJid); } if (!status.statusJidList?.length && !status.allContacts) { @@ -2532,7 +3254,11 @@ export class BaileysStartupService extends ChannelStartupService { return { content: { text: status.content }, - option: { backgroundColor: status.backgroundColor, font: status.font, statusJidList: status.statusJidList }, + option: { + backgroundColor: status.backgroundColor, + font: status.font, + statusJidList: status.statusJidList, + }, }; } if (status.type === 'image') { @@ -2553,7 +3279,11 @@ 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' }, + content: { + audio: convert, + ptt: true, + mimetype: 'audio/ogg; codecs=opus', + }, option: { statusJidList: status.statusJidList }, }; @@ -2573,14 +3303,17 @@ 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; } private async prepareMediaMessage(mediaMessage: MediaMessage) { try { - const type = mediaMessage.mediatype === 'ptv' ? 'video' : mediaMessage.mediatype; + const type = + mediaMessage.mediatype === 'ptv' ? 'video' : mediaMessage.mediatype; let mediaInput: any; if (mediaMessage.mediatype === 'image') { @@ -2620,7 +3353,7 @@ export class BaileysStartupService extends ChannelStartupService { { [type]: mediaInput, } as any, - { upload: this.client.waUploadToServer }, + { upload: this.client.waUploadToServer } ); const mediaType = mediaMessage.mediatype + 'Message'; @@ -2720,7 +3453,7 @@ export class BaileysStartupService extends ChannelStartupService { return generateWAMessageFromContent( '', { [mediaType]: { ...prepareMedia[mediaType] } }, - { userJid: this.instance.wuid }, + { userJid: this.instance.wuid } ); } catch (error) { this.logger.error(error); @@ -2733,7 +3466,10 @@ export class BaileysStartupService extends ChannelStartupService { let imageBuffer: Buffer; if (isBase64(image)) { - const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); + const base64Data = image.replace( + /^data:image\/(jpeg|png|gif);base64,/, + '' + ); imageBuffer = Buffer.from(base64Data, 'base64'); } else { const timestamp = new Date().getTime(); @@ -2763,7 +3499,9 @@ export class BaileysStartupService extends ChannelStartupService { const isAnimated = this.isAnimated(image, imageBuffer); if (isAnimated) { - return await sharp(imageBuffer, { animated: true }).webp({ quality: 80 }).toBuffer(); + return await sharp(imageBuffer, { animated: true }) + .webp({ quality: 80 }) + .toBuffer(); } else { return await sharp(imageBuffer).webp().toBuffer(); } @@ -2807,13 +3545,17 @@ export class BaileysStartupService extends ChannelStartupService { quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, - }, + } ); return result; } - public async mediaMessage(data: SendMediaDto, file?: any, isIntegration = false) { + public async mediaMessage( + data: SendMediaDto, + file?: any, + isIntegration = false + ) { const mediaData: SendMediaDto = { ...data }; if (file) mediaData.media = file.buffer.toString('base64'); @@ -2830,7 +3572,7 @@ export class BaileysStartupService extends ChannelStartupService { mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, }, - isIntegration, + isIntegration ); return mediaSent; @@ -2861,7 +3603,7 @@ export class BaileysStartupService extends ChannelStartupService { mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, }, - isIntegration, + isIntegration ); return mediaSent; @@ -2935,7 +3677,8 @@ export class BaileysStartupService extends ChannelStartupService { } public async processAudio(audio: string): Promise { - const audioConverterConfig = this.configService.get('AUDIO_CONVERTER'); + const audioConverterConfig = + this.configService.get('AUDIO_CONVERTER'); if (audioConverterConfig.API_URL) { this.logger.verbose('Using audio converter API'); const formData = new FormData(); @@ -2946,9 +3689,16 @@ 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( + audioConverterConfig.API_URL, + formData, + { + headers: { + ...formData.getHeaders(), + apikey: audioConverterConfig.API_KEY, + }, + } + ); if (!data.audio) { throw new InternalServerErrorException('Failed to convert audio'); @@ -2997,8 +3747,12 @@ export class BaileysStartupService extends ChannelStartupService { 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']); + this.logger.verbose( + 'Detected LPCM input – applying raw PCM settings' + ); + command = command + .inputFormat('s16le') + .inputOptions(['-ar', '24000', '-ac', '1']); } command @@ -3038,14 +3792,20 @@ export class BaileysStartupService extends ChannelStartupService { } } - public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { + public async audioWhatsapp( + data: SendAudioDto, + file?: any, + isIntegration = false + ) { const mediaData: SendAudioDto = { ...data }; if (file?.buffer) { mediaData.audio = file.buffer.toString('base64'); } else if (!isURL(data.audio) && !isBase64(data.audio)) { console.error('Invalid file or audio source'); - throw new BadRequestException('File buffer, URL, or base64 audio is required'); + throw new BadRequestException( + 'File buffer, URL, or base64 audio is required' + ); } if (!data?.encoding && data?.encoding !== false) { @@ -3060,7 +3820,7 @@ export class BaileysStartupService extends ChannelStartupService { data.number, { audio: convert, ptt: true, mimetype: 'audio/ogg; codecs=opus' }, { presence: 'recording', delay: data?.delay }, - isIntegration, + isIntegration ); return result; @@ -3072,12 +3832,14 @@ export class BaileysStartupService extends ChannelStartupService { return await this.sendMessageWithTyping( data.number, { - audio: isURL(data.audio) ? { url: data.audio } : Buffer.from(data.audio, 'base64'), + audio: isURL(data.audio) + ? { url: data.audio } + : Buffer.from(data.audio, 'base64'), ptt: true, mimetype: 'audio/ogg; codecs=opus', }, { presence: 'recording', delay: data?.delay }, - isIntegration, + isIntegration ); } @@ -3085,7 +3847,9 @@ export class BaileysStartupService extends ChannelStartupService { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let result = ''; for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); + result += characters.charAt( + Math.floor(Math.random() * characters.length) + ); } return result; } @@ -3094,10 +3858,24 @@ export class BaileysStartupService extends ChannelStartupService { const toString = (obj: any) => JSON.stringify(obj); const json = { - 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 }), + 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, + }), pix: () => toString({ currency: button.currency, @@ -3109,7 +3887,12 @@ export class BaileysStartupService extends ChannelStartupService { 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: [ @@ -3154,14 +3937,18 @@ export class BaileysStartupService extends ChannelStartupService { const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); - const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix'); + const hasOtherButtons = data.buttons.some( + (btn) => btn.type !== 'reply' && btn.type !== 'pix' + ); if (hasReplyButtons) { if (data.buttons.length > 3) { throw new BadRequestException('Maximum of 3 reply buttons allowed'); } if (hasOtherButtons) { - throw new BadRequestException('Reply buttons cannot be mixed with other button types'); + throw new BadRequestException( + 'Reply buttons cannot be mixed with other button types' + ); } } @@ -3170,7 +3957,9 @@ export class BaileysStartupService extends ChannelStartupService { throw new BadRequestException('Only one PIX button is allowed'); } if (hasOtherButtons) { - throw new BadRequestException('PIX button cannot be mixed with other button types'); + throw new BadRequestException( + 'PIX button cannot be mixed with other button types' + ); } const message: proto.IMessage = { @@ -3178,8 +3967,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(), + }), }, }, }, @@ -3197,12 +3994,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 = { @@ -3231,7 +4034,10 @@ export class BaileysStartupService extends ChannelStartupService { })(), nativeFlowMessage: { buttons: buttons, - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), + messageParamsJson: JSON.stringify({ + from: 'api', + templateId: v4(), + }), }, }, }, @@ -3264,7 +4070,7 @@ export class BaileysStartupService extends ChannelStartupService { quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, - }, + } ); } @@ -3287,7 +4093,7 @@ export class BaileysStartupService extends ChannelStartupService { quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, - }, + } ); } @@ -3295,7 +4101,11 @@ export class BaileysStartupService extends ChannelStartupService { const message: proto.IMessage = {}; const vcard = (contact: ContactMessage) => { - let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; + let result = + 'BEGIN:VCARD\n' + + 'VERSION:3.0\n' + + `N:${contact.fullName}\n` + + `FN:${contact.fullName}\n`; if (contact.organization) { result += `ORG:${contact.organization};\n`; @@ -3313,13 +4123,19 @@ export class BaileysStartupService extends ChannelStartupService { contact.wuid = createJid(contact.phoneNumber); } - result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; + result += + `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + + 'item1.X-ABLabel:Celular\n' + + 'END:VCARD'; return result; }; 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`, @@ -3361,7 +4177,11 @@ export class BaileysStartupService extends ChannelStartupService { const onWhatsapp: OnWhatsAppDto[] = []; // BROADCAST - onWhatsapp.push(...jids.broadcast.map(({ jid, number }) => new OnWhatsAppDto(jid, false, number))); + onWhatsapp.push( + ...jids.broadcast.map( + ({ jid, number }) => new OnWhatsAppDto(jid, false, number) + ) + ); // GROUPS const groups = await Promise.all( @@ -3373,13 +4193,16 @@ export class BaileysStartupService extends ChannelStartupService { } return new OnWhatsAppDto(group.id, true, 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) @@ -3389,22 +4212,32 @@ export class BaileysStartupService extends ChannelStartupService { const cachedNumbers = await getOnWhatsappCache(numbersToVerify); // 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)); + 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')); + 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)`); + this.logger.verbose( + `Checking ${normalNumbersNotInCache.length} numbers via Baileys (not found in cache)` + ); verify = await this.client.onWhatsApp(...normalNumbersNotInCache); } const verifiedUsers = 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('+', ''))); + const cached = cachedNumbers.find((cached) => + cached.jidOptions.includes(user.jid.replace('+', '')) + ); if (cached) { this.logger.verbose(`Number ${user.number} found in cache`); @@ -3413,7 +4246,8 @@ export class BaileysStartupService extends ChannelStartupService { true, user.number, contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName, - cached.lid || (cached.remoteJid.includes('@lid') ? 'lid' : undefined), + cached.lid || + (cached.remoteJid.includes('@lid') ? 'lid' : undefined) ); } @@ -3424,7 +4258,7 @@ export class BaileysStartupService extends ChannelStartupService { true, user.number, contacts.find((c) => c.remoteJid === user.jid)?.pushName, - 'lid', + 'lid' ); } @@ -3438,16 +4272,23 @@ export class BaileysStartupService extends ChannelStartupService { ? user.number : `${user.number.slice(0, 4)}9${user.number.slice(4)}`; const numberWithoutDigit = - user.number.length === 12 ? user.number : user.number.slice(0, 4) + user.number.slice(5); + user.number.length === 12 + ? user.number + : user.number.slice(0, 4) + user.number.slice(5); numberVerified = verify.find( - (v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`, + (v) => + v.jid === `${numberWithDigit}@s.whatsapp.net` || + v.jid === `${numberWithoutDigit}@s.whatsapp.net` ); } // Mexican/Argentina numbers // Ref: https://faq.whatsapp.com/1294841057948784 - if (!numberVerified && (user.number.startsWith('52') || user.number.startsWith('54'))) { + if ( + !numberVerified && + (user.number.startsWith('52') || user.number.startsWith('54')) + ) { let prefix = ''; if (user.number.startsWith('52')) { prefix = '1'; @@ -3461,10 +4302,14 @@ export class BaileysStartupService extends ChannelStartupService { ? user.number : `${user.number.slice(0, 2)}${prefix}${user.number.slice(2)}`; const numberWithoutDigit = - user.number.length === 12 ? user.number : user.number.slice(0, 2) + user.number.slice(3); + user.number.length === 12 + ? user.number + : user.number.slice(0, 2) + user.number.slice(3); numberVerified = verify.find( - (v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`, + (v) => + v.jid === `${numberWithDigit}@s.whatsapp.net` || + v.jid === `${numberWithoutDigit}@s.whatsapp.net` ); } @@ -3479,9 +4324,9 @@ export class BaileysStartupService extends ChannelStartupService { !!numberVerified?.exists, user.number, contacts.find((c) => c.remoteJid === numberJid)?.pushName, - undefined, + undefined ); - }), + }) ); // Combine results @@ -3491,7 +4336,9 @@ export class BaileysStartupService extends ChannelStartupService { 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('+', ''))); + const cached = cachedNumbers?.find((cached) => + cached.jidOptions.includes(user.jid.replace('+', '')) + ); return !cached; }); @@ -3501,7 +4348,7 @@ export class BaileysStartupService extends ChannelStartupService { numbersToCache.map((user) => ({ remoteJid: user.jid, lid: user.lid === 'lid' ? 'lid' : undefined, - })), + })) ); } @@ -3513,18 +4360,28 @@ export class BaileysStartupService extends ChannelStartupService { 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 }); + keys.push({ + remoteJid: read.remoteJid, + fromMe: read.fromMe, + id: read.id, + }); } }); await this.client.readMessages(keys); return { message: 'Read messages', read: 'success' }; } catch (error) { - throw new InternalServerErrorException('Read messages fail', error.toString()); + throw new InternalServerErrorException( + 'Read messages fail', + error.toString() + ); } } 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, @@ -3556,7 +4413,8 @@ export class BaileysStartupService extends ChannelStartupService { last_message = await this.getLastMessage(number); } else { last_message = data.lastMessage; - last_message.messageTimestamp = last_message?.messageTimestamp ?? Date.now(); + last_message.messageTimestamp = + last_message?.messageTimestamp ?? Date.now(); number = last_message?.key?.remoteJid; } @@ -3564,13 +4422,19 @@ 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 }; } catch (error) { throw new InternalServerErrorException({ archived: false, - message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], + message: [ + 'An error occurred while archiving the chat. Open a calling.', + error.toString(), + ], }); } } @@ -3584,7 +4448,8 @@ export class BaileysStartupService extends ChannelStartupService { last_message = await this.getLastMessage(number); } else { last_message = data.lastMessage; - last_message.messageTimestamp = last_message?.messageTimestamp ?? Date.now(); + last_message.messageTimestamp = + last_message?.messageTimestamp ?? Date.now(); number = last_message?.key?.remoteJid; } @@ -3592,35 +4457,54 @@ 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 }; } catch (error) { throw new InternalServerErrorException({ markedChatUnread: false, - message: ['An error occurred while marked unread the chat. Open a calling.', error.toString()], + message: [ + 'An error occurred while marked unread the chat. Open a calling.', + error.toString(), + ], }); } } public async deleteMessage(del: DeleteMessage) { try { - const response = await this.client.sendMessage(del.remoteJid, { delete: del }); + const response = await this.client.sendMessage(del.remoteJid, { + delete: del, + }); if (response) { const messageId = response.message?.protocolMessage?.key?.id; if (messageId) { - const isLogicalDeleted = configService.get('DATABASE').DELETE_DATA.LOGICAL_MESSAGE_DELETE; + const isLogicalDeleted = + configService.get('DATABASE').DELETE_DATA + .LOGICAL_MESSAGE_DELETE; let message = await this.prismaRepository.message.findFirst({ where: { key: { path: ['id'], equals: messageId } }, }); if (isLogicalDeleted) { if (!message) return response; - const existingKey = typeof message?.key === 'object' && message.key !== null ? message.key : {}; + 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' }, + data: { + key: { ...existingKey, deleted: true }, + status: 'DELETED', + }, }); - if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) { + if ( + this.configService.get('DATABASE').SAVE_DATA + .MESSAGE_UPDATE + ) { const messageUpdate: any = { messageId: message.id, keyId: messageId, @@ -3630,11 +4514,15 @@ export class BaileysStartupService extends ChannelStartupService { status: 'DELETED', instanceId: this.instanceId, }; - await this.prismaRepository.messageUpdate.create({ data: messageUpdate }); + 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: message.id }, + }); } this.sendDataWebhook(Events.MESSAGES_DELETE, { id: message.id, @@ -3653,7 +4541,10 @@ export class BaileysStartupService extends ChannelStartupService { return response; } catch (error) { - throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); + throw new InternalServerErrorException( + 'Error while deleting message for everyone', + error?.toString() + ); } } @@ -3669,12 +4560,17 @@ export class BaileysStartupService extends ChannelStartupService { return map[mediaType] || null; } - public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) { + public async getBase64FromMediaMessage( + data: getBase64FromMediaMessageDto, + getBuffer = false + ) { try { const m = data?.message; const convertToMp4 = data?.convertToMp4 ?? false; - const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + const msg = m?.message + ? m + : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); if (!msg) { throw 'Message not found'; @@ -3686,7 +4582,10 @@ export class BaileysStartupService extends ChannelStartupService { } } - if ('messageContextInfo' in msg.message && Object.keys(msg.message).length === 1) { + if ( + 'messageContextInfo' in msg.message && + Object.keys(msg.message).length === 1 + ) { throw 'The message is messageContextInfo'; } @@ -3695,13 +4594,16 @@ export class BaileysStartupService extends ChannelStartupService { if (msg.message?.templateMessage) { const template = - msg.message.templateMessage.hydratedTemplate || msg.message.templateMessage.hydratedFourRowTemplate; + 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 } }; + msg.message = { + [type]: { ...template[type], url: template[type].staticUrl }, + }; break; } } @@ -3724,7 +4626,9 @@ export class BaileysStartupService extends ChannelStartupService { } if (typeof mediaMessage['mediaKey'] === 'object') { - msg.message[mediaType].mediaKey = Uint8Array.from(Object.values(mediaMessage['mediaKey'])); + msg.message[mediaType].mediaKey = Uint8Array.from( + Object.values(mediaMessage['mediaKey']) + ); } let buffer: Buffer; @@ -3734,13 +4638,21 @@ export class BaileysStartupService extends ChannelStartupService { { key: msg?.key, message: msg?.message }, 'buffer', {}, - { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + } ); } catch { - this.logger.error('Download Media failed, trying to retry in 5 seconds...'); + 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'); + 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( @@ -3750,23 +4662,30 @@ export class BaileysStartupService extends ChannelStartupService { 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!'); + this.logger.info( + 'Download Media with downloadContentFromMessage was successful!' + ); } catch (fallbackErr) { - this.logger.error('Download Media with downloadContentFromMessage also failed!'); + this.logger.error( + 'Download Media with downloadContentFromMessage also failed!' + ); throw fallbackErr; } } const typeMessage = getContentType(msg.message); const ext = mimeTypes.extension(mediaMessage?.['mimetype']); - const fileName = mediaMessage?.['fileName'] || `${msg.key.id}.${ext}` || `${v4()}.${ext}`; + const fileName = + mediaMessage?.['fileName'] || + `${msg.key.id}.${ext}` || + `${v4()}.${ext}`; if (convertToMp4 && typeMessage === 'audioMessage') { try { @@ -3800,7 +4719,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, @@ -3848,7 +4771,10 @@ export class BaileysStartupService extends ChannelStartupService { }, }; } catch (error) { - throw new InternalServerErrorException('Error updating privacy settings', error.toString()); + throw new InternalServerErrorException( + 'Error updating privacy settings', + error.toString() + ); } } @@ -3861,12 +4787,19 @@ 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 }; } catch (error) { - throw new InternalServerErrorException('Error updating profile name', error.toString()); + throw new InternalServerErrorException( + 'Error updating profile name', + error.toString() + ); } } @@ -3876,7 +4809,10 @@ export class BaileysStartupService extends ChannelStartupService { return { update: 'success' }; } catch (error) { - throw new InternalServerErrorException('Error updating profile name', error.toString()); + throw new InternalServerErrorException( + 'Error updating profile name', + error.toString() + ); } } @@ -3886,7 +4822,10 @@ export class BaileysStartupService extends ChannelStartupService { return { update: 'success' }; } catch (error) { - throw new InternalServerErrorException('Error updating profile status', error.toString()); + throw new InternalServerErrorException( + 'Error updating profile status', + error.toString() + ); } } @@ -3918,7 +4857,9 @@ export class BaileysStartupService extends ChannelStartupService { } else if (isBase64(picture)) { pic = Buffer.from(picture, 'base64'); } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); + throw new BadRequestException( + ''profilePicture' must be a url or a base64' + ); } await this.client.updateProfilePicture(this.instance.wuid, pic); @@ -3927,7 +4868,10 @@ export class BaileysStartupService extends ChannelStartupService { return { update: 'success' }; } catch (error) { - throw new InternalServerErrorException('Error updating profile picture', error.toString()); + throw new InternalServerErrorException( + 'Error updating profile picture', + error.toString() + ); } } @@ -3939,7 +4883,10 @@ export class BaileysStartupService extends ChannelStartupService { return { update: 'success' }; } catch (error) { - throw new InternalServerErrorException('Error removing profile picture', error.toString()); + throw new InternalServerErrorException( + 'Error removing profile picture', + error.toString() + ); } } @@ -3949,7 +4896,11 @@ export class BaileysStartupService extends ChannelStartupService { const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); - if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + if ( + !isWA.exists && + !isJidGroup(isWA.jid) && + !isWA.jid.includes('@broadcast') + ) { throw new BadRequestException(isWA); } @@ -3959,7 +4910,10 @@ export class BaileysStartupService extends ChannelStartupService { return { block: 'success' }; } catch (error) { - throw new InternalServerErrorException('Error blocking user', error.toString()); + throw new InternalServerErrorException( + 'Error blocking user', + error.toString() + ); } } @@ -3971,7 +4925,10 @@ export class BaileysStartupService extends ChannelStartupService { const msg: any = await this.getMessage(data.key, true); - if (msg?.messageType === 'conversation' || msg?.messageType === 'extendedTextMessage') { + if ( + msg?.messageType === 'conversation' || + msg?.messageType === 'extendedTextMessage' + ) { return { text: data.text }; } @@ -4013,22 +4970,35 @@ export class BaileysStartupService extends ChannelStartupService { } } - const messageSent = await this.client.sendMessage(jid, { ...(options as any), edit: data.key }); + 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; + 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) + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) this.chatwootService.eventWhatsapp( 'send.message.update', - { instanceName: this.instance.name, instanceId: this.instance.id }, - editedMessage, + { + 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) { + if ( + messageId && + this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE + ) { let message = await this.prismaRepository.message.findFirst({ where: { key: { path: ['id'], equals: messageId } }, }); @@ -4041,7 +5011,10 @@ export class BaileysStartupService extends ChannelStartupService { new BadRequestException('You cannot edit deleted messages'); } - if (oldMessage.messageType === 'conversation' || oldMessage.messageType === 'extendedTextMessage') { + if ( + oldMessage.messageType === 'conversation' || + oldMessage.messageType === 'extendedTextMessage' + ) { oldMessage.message.conversation = data.text; } else { oldMessage.message[oldMessage.messageType].caption = data.text; @@ -4055,7 +5028,10 @@ export class BaileysStartupService extends ChannelStartupService { }, }); - if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) { + if ( + this.configService.get('DATABASE').SAVE_DATA + .MESSAGE_UPDATE + ) { const messageUpdate: any = { messageId: message.id, keyId: messageId, @@ -4065,7 +5041,9 @@ export class BaileysStartupService extends ChannelStartupService { status: 'EDITED', instanceId: this.instanceId, }; - await this.prismaRepository.messageUpdate.create({ data: messageUpdate }); + await this.prismaRepository.messageUpdate.create({ + data: messageUpdate, + }); } } } @@ -4079,7 +5057,9 @@ export class BaileysStartupService extends ChannelStartupService { } 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, @@ -4090,7 +5070,9 @@ export class BaileysStartupService extends ChannelStartupService { } public async handleLabel(data: HandleLabelDto) { - const whatsappContact = await this.whatsappNumber({ numbers: [data.number] }); + const whatsappContact = await this.whatsappNumber({ + numbers: [data.number], + }); if (whatsappContact.length === 0) { throw new NotFoundException('Number not found'); } @@ -4113,7 +5095,10 @@ export class BaileysStartupService extends ChannelStartupService { return { numberJid: contact.jid, labelId: data.labelId, remove: true }; } } catch (error) { - throw new BadRequestException(`Unable to ${data.action} label to chat`, error.toString()); + throw new BadRequestException( + `Unable to ${data.action} label to chat`, + error.toString() + ); } } @@ -4124,9 +5109,15 @@ export class BaileysStartupService extends ChannelStartupService { const cacheConf = this.configService.get('CACHE'); - if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) { + 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; @@ -4141,7 +5132,10 @@ export class BaileysStartupService extends ChannelStartupService { const cacheConf = this.configService.get('CACHE'); - if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) { + if ( + (cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || + cacheConf?.LOCAL?.ENABLED + ) { if (await groupMetadataCache?.has(groupJid)) { console.log(`Cache request for group: ${groupJid}`); const meta = await groupMetadataCache.get(groupJid); @@ -4162,17 +5156,26 @@ export class BaileysStartupService extends ChannelStartupService { public async createGroup(create: CreateGroupDto) { try { - const participants = (await this.whatsappNumber({ numbers: create.participants })) + const participants = ( + await this.whatsappNumber({ numbers: create.participants }) + ) .filter((participant) => participant.exists) .map((participant) => participant.jid); - const { id } = await this.client.groupCreate(create.subject, participants); + const { id } = await this.client.groupCreate( + create.subject, + participants + ); if (create?.description) { await this.client.groupUpdateDescription(id, create.description); } 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); @@ -4180,7 +5183,10 @@ export class BaileysStartupService extends ChannelStartupService { return group; } catch (error) { this.logger.error(error); - throw new InternalServerErrorException('Error creating group', error.toString()); + throw new InternalServerErrorException( + 'Error creating group', + error.toString() + ); } } @@ -4212,13 +5218,18 @@ export class BaileysStartupService extends ChannelStartupService { } else if (isBase64(picture.image)) { pic = Buffer.from(picture.image, 'base64'); } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); + throw new BadRequestException( + ''profilePicture' must be a url or a base64' + ); } await this.client.updateProfilePicture(picture.groupJid, pic); return { update: 'success' }; } catch (error) { - throw new InternalServerErrorException('Error update group picture', error.toString()); + throw new InternalServerErrorException( + 'Error update group picture', + error.toString() + ); } } @@ -4228,7 +5239,10 @@ export class BaileysStartupService extends ChannelStartupService { return { update: 'success' }; } catch (error) { - throw new InternalServerErrorException('Error updating group subject', error.toString()); + throw new InternalServerErrorException( + 'Error updating group subject', + error.toString() + ); } } @@ -4238,7 +5252,10 @@ export class BaileysStartupService extends ChannelStartupService { return { update: 'success' }; } catch (error) { - throw new InternalServerErrorException('Error updating group description', error.toString()); + throw new InternalServerErrorException( + 'Error updating group description', + error.toString() + ); } } @@ -4280,7 +5297,9 @@ export class BaileysStartupService extends ChannelStartupService { } public async fetchAllGroups(getParticipants: GetParticipant) { - const fetch = Object.values(await this?.client?.groupFetchAllParticipating()); + const fetch = Object.values( + await this?.client?.groupFetchAllParticipating() + ); let groups = []; for (const group of fetch) { @@ -4317,7 +5336,10 @@ export class BaileysStartupService extends ChannelStartupService { public async inviteCode(id: GroupJid) { try { const code = await this.client.groupInviteCode(id.groupJid); - return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; + return { + inviteUrl: `https://chat.whatsapp.com/${code}`, + inviteCode: code, + }; } catch (error) { throw new NotFoundException('No invite code', error.toString()); } @@ -4374,9 +5396,13 @@ export class BaileysStartupService extends ChannelStartupService { public async findParticipants(id: GroupJid) { try { - const participants = (await this.client.groupMetadata(id.groupJid)).participants; + 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); @@ -4387,9 +5413,13 @@ export class BaileysStartupService extends ChannelStartupService { }; }); - const usersContacts = parsedParticipants.filter((c) => c.id.includes('@s.whatsapp')); + const usersContacts = parsedParticipants.filter((c) => + c.id.includes('@s.whatsapp') + ); if (usersContacts) { - await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.id }))); + await saveOnWhatsappCache( + usersContacts.map((c) => ({ remoteJid: c.id })) + ); } return { participants: parsedParticipants }; @@ -4405,17 +5435,23 @@ export class BaileysStartupService extends ChannelStartupService { const updateParticipants = await this.client.groupParticipantsUpdate( update.groupJid, participants, - update.action, + update.action ); return { updateParticipants: updateParticipants }; } catch (error) { - throw new BadRequestException('Error updating participants', error.toString()); + throw new BadRequestException( + 'Error updating participants', + error.toString() + ); } } public async updateGSetting(update: GroupUpdateSettingDto) { try { - const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); + const updateSetting = await this.client.groupSettingUpdate( + update.groupJid, + update.action + ); return { updateSetting: updateSetting }; } catch (error) { throw new BadRequestException('Error updating setting', error.toString()); @@ -4424,7 +5460,10 @@ export class BaileysStartupService extends ChannelStartupService { public async toggleEphemeral(update: GroupToggleEphemeralDto) { try { - await this.client.groupToggleEphemeral(update.groupJid, update.expiration); + await this.client.groupToggleEphemeral( + update.groupJid, + update.expiration + ); return { success: true }; } catch (error) { throw new BadRequestException('Error updating setting', error.toString()); @@ -4436,7 +5475,10 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.groupLeave(id.groupJid); return { groupJid: id.groupJid, leave: true }; } catch (error) { - throw new BadRequestException('Unable to leave the group', error.toString()); + throw new BadRequestException( + 'Unable to leave the group', + error.toString() + ); } } @@ -4449,12 +5491,18 @@ export class BaileysStartupService extends ChannelStartupService { return obj; } - if (typeof obj === 'object' && !Array.isArray(obj) && !Buffer.isBuffer(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]); + const values = keys + .sort((a, b) => Number(a) - Number(b)) + .map((key) => obj[key]); return new Uint8Array(values); } } @@ -4493,7 +5541,10 @@ export class BaileysStartupService extends ChannelStartupService { message.pushName || (message.key.fromMe ? 'Você' - : message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)), + : message?.participant || + (message.key?.participant + ? message.key.participant.split('@')[0] + : null)), status: status[message.status], message: this.deserializeMessageBuffers({ ...message.message }), contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo), @@ -4511,13 +5562,15 @@ export class BaileysStartupService extends ChannelStartupService { if (messageRaw.message.extendedTextMessage) { messageRaw.messageType = 'conversation'; - messageRaw.message.conversation = messageRaw.message.extendedTextMessage.text; + messageRaw.message.conversation = + messageRaw.message.extendedTextMessage.text; delete messageRaw.message.extendedTextMessage; } if (messageRaw.message.documentWithCaptionMessage) { messageRaw.messageType = 'documentMessage'; - messageRaw.message.documentMessage = messageRaw.message.documentWithCaptionMessage.message.documentMessage; + messageRaw.message.documentMessage = + messageRaw.message.documentWithCaptionMessage.message.documentMessage; delete messageRaw.message.documentWithCaptionMessage; } @@ -4529,7 +5582,8 @@ export class BaileysStartupService extends ChannelStartupService { } if (quotedMessage.documentWithCaptionMessage) { - quotedMessage.documentMessage = quotedMessage.documentWithCaptionMessage.message.documentMessage; + quotedMessage.documentMessage = + quotedMessage.documentWithCaptionMessage.message.documentMessage; delete quotedMessage.documentWithCaptionMessage; } } @@ -4538,15 +5592,24 @@ export class BaileysStartupService extends ChannelStartupService { } private async syncChatwootLostMessages() { - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { const chatwootConfig = await this.findChatwoot(); const prepare = (message: any) => this.prepareMessage(message); - this.chatwootService.syncLostMessages({ instanceName: this.instance.name }, chatwootConfig, prepare); + 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); + 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) @@ -4554,29 +5617,38 @@ export class BaileysStartupService extends ChannelStartupService { 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}`); + this.logger.info( + `Stopping syncChatwootLostMessages cron - ID mismatch: ${cronId} vs ${storedId}` + ); task.stop(); return; } } - this.chatwootService.syncLostMessages({ instanceName: this.instance.name }, chatwootConfig, prepare); + this.chatwootService.syncLostMessages( + { instanceName: this.instance.name }, + chatwootConfig, + prepare + ); }); task.start(); } } - private async updateMessagesReadedByTimestamp(remoteJid: string, timestamp?: number): Promise { + 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]}) + 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]}) `; if (result) { @@ -4595,16 +5667,19 @@ export class BaileysStartupService extends ChannelStartupService { 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]} + 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), ]); 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; @@ -4614,47 +5689,51 @@ export class BaileysStartupService extends ChannelStartupService { 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") + `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" = ( + SET 'labels' = ( SELECT to_jsonb(array_agg(DISTINCT elem)) FROM ( - SELECT jsonb_array_elements_text("Chat"."labels") AS elem + SELECT jsonb_array_elements_text('Chat'.'labels') AS elem UNION SELECT $1::text AS elem ) sub ), - "updatedAt" = NOW();`, + 'updatedAt' = NOW();`, labelId, instanceId, chatId, - id, + id ); } - private async removeLabel(labelId: string, instanceId: string, chatId: string) { + 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") + `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 ( + SET 'labels' = COALESCE ( ( SELECT jsonb_agg(elem) - FROM jsonb_array_elements_text("Chat"."labels") AS elem + FROM jsonb_array_elements_text('Chat'.'labels') AS elem WHERE elem <> $1 ), '[]'::jsonb ), - "updatedAt" = NOW();`, + 'updatedAt' = NOW();`, labelId, instanceId, chatId, - id, + id ); } @@ -4664,7 +5743,11 @@ export class BaileysStartupService extends ChannelStartupService { return response; } - public async baileysProfilePictureUrl(jid: string, type: 'image' | 'preview', timeoutMs: number) { + public async baileysProfilePictureUrl( + jid: string, + type: 'image' | 'preview', + timeoutMs: number + ) { const response = await this.client.profilePictureUrl(jid, type, timeoutMs); return response; @@ -4676,8 +5759,16 @@ export class BaileysStartupService extends ChannelStartupService { return response; } - public async baileysCreateParticipantNodes(jids: string[], message: proto.IMessage, extraAttrs: any) { - const response = await this.client.createParticipantNodes(jids, message, extraAttrs); + public async baileysCreateParticipantNodes( + jids: string[], + message: proto.IMessage, + extraAttrs: any + ) { + const response = await this.client.createParticipantNodes( + jids, + message, + extraAttrs + ); const convertedResponse = { ...response, @@ -4685,7 +5776,10 @@ export class BaileysStartupService extends ChannelStartupService { ...node, content: node.content?.map((c: any) => ({ ...c, - content: c.content instanceof Uint8Array ? Buffer.from(c.content).toString('base64') : c.content, + content: + c.content instanceof Uint8Array + ? Buffer.from(c.content).toString('base64') + : c.content, })), })), }; @@ -4700,8 +5794,16 @@ export class BaileysStartupService extends ChannelStartupService { return response; } - public async baileysGetUSyncDevices(jids: string[], useCache: boolean, ignoreZeroDevices: boolean) { - const response = await this.client.getUSyncDevices(jids, useCache, ignoreZeroDevices); + public async baileysGetUSyncDevices( + jids: string[], + useCache: boolean, + ignoreZeroDevices: boolean + ) { + const response = await this.client.getUSyncDevices( + jids, + useCache, + ignoreZeroDevices + ); return response; } @@ -4712,13 +5814,23 @@ export class BaileysStartupService extends ChannelStartupService { return response; } - public async baileysSignalRepositoryDecryptMessage(jid: string, type: 'pkmsg' | 'msg', ciphertext: string) { + 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 }); + const response = await this.client.signalRepository.decryptMessage({ + jid, + type, + ciphertext: ciphertextBuffer, + }); - return response instanceof Uint8Array ? Buffer.from(response).toString('base64') : response; + return response instanceof Uint8Array + ? Buffer.from(response).toString('base64') + : response; } catch (error) { this.logger.error('Error decrypting message:'); this.logger.error(error); @@ -4727,7 +5839,10 @@ export class BaileysStartupService extends ChannelStartupService { } public async baileysGetAuthState() { - const response = { me: this.client.authState.creds.me, account: this.client.authState.creds.account }; + const response = { + me: this.client.authState.creds.me, + account: this.client.authState.creds.account, + }; return response; } @@ -4750,7 +5865,9 @@ export class BaileysStartupService extends ChannelStartupService { let catalog = await this.getCatalog({ jid: info?.jid, limit, cursor }); let nextPageCursor = catalog.nextPageCursor; - let nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; + let nextPageCursorJson = nextPageCursor + ? JSON.parse(atob(nextPageCursor)) + : null; let pagination = nextPageCursorJson?.pagination_cursor ? JSON.parse(atob(nextPageCursorJson.pagination_cursor)) : null; @@ -4759,9 +5876,15 @@ export class BaileysStartupService extends ChannelStartupService { let productsCatalog = catalog.products || []; let countLoops = 0; while (fetcherHasMore && countLoops < 4) { - catalog = await this.getCatalog({ jid: info?.jid, limit, cursor: nextPageCursor }); + catalog = await this.getCatalog({ + jid: info?.jid, + limit, + cursor: nextPageCursor, + }); nextPageCursor = catalog.nextPageCursor; - nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; + nextPageCursorJson = nextPageCursor + ? JSON.parse(atob(nextPageCursor)) + : null; pagination = nextPageCursorJson?.pagination_cursor ? JSON.parse(atob(nextPageCursorJson.pagination_cursor)) : null; @@ -4783,15 +5906,18 @@ export class BaileysStartupService extends ChannelStartupService { } } - public async getCatalog({ - jid, - limit, - cursor, - }: GetCatalogOptions): Promise<{ products: Product[]; nextPageCursor: string | undefined }> { + 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 }); + const catalog = await this.client.getCatalog({ + jid, + limit: limit, + cursor: cursor, + }); if (!catalog) { return { products: undefined, nextPageCursor: undefined }; @@ -4799,7 +5925,10 @@ export class BaileysStartupService extends ChannelStartupService { return catalog; } catch (error) { - throw new InternalServerErrorException('Error getCatalog', error.toString()); + throw new InternalServerErrorException( + 'Error getCatalog', + error.toString() + ); } } @@ -4831,19 +5960,27 @@ export class BaileysStartupService extends ChannelStartupService { } } - public async getCollections(jid?: string | undefined, limit?: number): Promise { + 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 [ + { id: undefined, name: undefined, products: [], status: undefined }, + ]; } return result.collections; } catch (error) { - throw new InternalServerErrorException('Error getCatalog', error.toString()); + throw new InternalServerErrorException( + 'Error getCatalog', + error.toString() + ); } } @@ -4852,10 +5989,17 @@ export class BaileysStartupService extends ChannelStartupService { const timestampFilter = {}; if (query?.where?.messageTimestamp) { - if (query.where.messageTimestamp['gte'] && query.where.messageTimestamp['lte']) { + 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), + gte: Math.floor( + new Date(query.where.messageTimestamp['gte']).getTime() / 1000 + ), + lte: Math.floor( + new Date(query.where.messageTimestamp['lte']).getTime() / 1000 + ), }; } } @@ -4868,14 +6012,35 @@ export class BaileysStartupService extends ChannelStartupService { messageType: query?.where?.messageType, ...timestampFilter, AND: [ - keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {}, - keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {}, - keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {}, - keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {}, + keyFilters?.id + ? { key: { path: ['id'], equals: keyFilters?.id } } + : {}, + keyFilters?.fromMe + ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } + : {}, + keyFilters?.remoteJid + ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } + : {}, + 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 } } : {}, + keyFilters?.remoteJid + ? { + key: { path: ['remoteJid'], equals: keyFilters?.remoteJid }, + } + : {}, + keyFilters?.remoteJidAlt + ? { + key: { + path: ['remoteJidAlt'], + equals: keyFilters?.remoteJidAlt, + }, + } + : {}, ], }, ], @@ -4898,20 +6063,42 @@ export class BaileysStartupService extends ChannelStartupService { messageType: query?.where?.messageType, ...timestampFilter, AND: [ - keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {}, - keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {}, - keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {}, - keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {}, + keyFilters?.id + ? { key: { path: ['id'], equals: keyFilters?.id } } + : {}, + keyFilters?.fromMe + ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } + : {}, + keyFilters?.remoteJid + ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } + : {}, + 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 } } : {}, + 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), + skip: + query.offset * (query?.page === 1 ? 0 : (query?.page as number) - 1), take: query.offset, select: { id: true, @@ -4928,7 +6115,12 @@ export class BaileysStartupService extends ChannelStartupService { }); const formattedMessages = messages.map((message) => { - const messageKey = message.key as { fromMe: boolean; remoteJid: string; id: string; participant?: string }; + const messageKey = message.key as { + fromMe: boolean; + remoteJid: string; + id: string; + participant?: string; + }; if (!message.pushName) { if (messageKey.fromMe) { diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index cc2bd9e4d..8d2173920 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -53,7 +53,7 @@ export class ChatwootService { private readonly configService: ConfigService, private readonly prismaRepository: PrismaRepository, private readonly cache: CacheService, - ) {} + ) { } private pgClient = postgresClient.getChatwootConnection(); @@ -720,8 +720,8 @@ export class ChatwootService { contact.name === chatId || (`+${chatId}`.startsWith('+55') ? this.getNumbers(`+${chatId}`).some( - (v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1), - ) + (v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1), + ) : false); this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`); this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`); @@ -1288,10 +1288,10 @@ export class ChatwootService { // Chatwoot to Whatsapp const messageReceived = body.content ? body.content - .replaceAll(/(? {}; + fileStream._read = () => { }; fileStream.push(fileData); fileStream.push(null); @@ -2142,7 +2142,7 @@ export class ChatwootService { const processedBuffer = await img.getBuffer(JimpMime.png); const fileStream = new Readable(); - fileStream._read = () => {}; // _read is required but you can noop it + fileStream._read = () => { }; // _read is required but you can noop it fileStream.push(processedBuffer); fileStream.push(null); @@ -2237,56 +2237,103 @@ export class ChatwootService { return send; } } - + // DELETE + // Hard delete quando habilitado; senão cria placeholder "apagada pelo remetente" if (event === Events.MESSAGES_DELETE) { + // Anti-dup local (process-wide) por 15s + const dedupKey = `cw_del_${instance.instanceId}_${body?.key?.id}`; + const g = (global as any); + if (!g.__cwDel) g.__cwDel = new Map(); + const last = g.__cwDel.get(dedupKey); + const now = Date.now(); + if (last && now - last < 15000) { + this.logger.info(`[CW.DELETE] Ignorado (duplicado local) para ${body?.key?.id}`); + return; + } + g.__cwDel.set(dedupKey, now); + const chatwootDelete = this.configService.get('CHATWOOT').MESSAGE_DELETE; - if (chatwootDelete === true) { - if (!body?.key?.id) { - this.logger.warn('message id not found'); - return; - } + if (!body?.key?.id) { + this.logger.warn('message id not found'); + return; + } - const message = await this.getMessageByKeyId(instance, body.key.id); + const message = await this.getMessageByKeyId(instance, body.key.id); + if (!message) { + this.logger.warn('Message not found for delete event'); + return; + } - if (message?.chatwootMessageId && message?.chatwootConversationId) { - await this.prismaRepository.message.deleteMany({ - where: { - key: { - path: ['id'], - equals: body.key.id, - }, - instanceId: instance.instanceId, - }, - }); + if (chatwootDelete === true && message?.chatwootMessageId && message?.chatwootConversationId) { + await this.prismaRepository.message.deleteMany({ + where: { + key: { path: ['id'], equals: body.key.id }, + instanceId: instance.instanceId, + }, + }); - return await client.messages.delete({ - accountId: this.provider.accountId, - conversationId: message.chatwootConversationId, - messageId: message.chatwootMessageId, - }); + await client.messages.delete({ + accountId: this.provider.accountId, + conversationId: message.chatwootConversationId, + messageId: message.chatwootMessageId, + }); + return; // hard delete + } else { + const key = message.key as WAMessageKey; + const messageType = key?.fromMe ? 'outgoing' : 'incoming'; + const DELETE_PLACEHOLDER = '🗑️ Mensagem apagada pelo remetente'; + + if (message.chatwootConversationId) { + const send = await this.createMessage( + instance, + message.chatwootConversationId, + DELETE_PLACEHOLDER, + messageType, + false, + [], + { message: { extendedTextMessage: { contextInfo: { stanzaId: key.id } } } }, + 'DEL:' + body.key.id, // mantém a intenção de idempotência + null, + ); + if (!send) this.logger.warn('delete placeholder not sent'); } + return; } } + // EDIT + // Cria "Mensagem editada: " SOMENTE se houver texto (evita 'undefined') + // Se vier "edit" sem texto (REVOKE mascarado), não faz nada aqui — o bloco de DELETE trata. if (event === 'messages.edit' || event === 'send.message.update') { - const editedMessageContent = - body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text; - const message = await this.getMessageByKeyId(instance, body?.key?.id); + const editedMessageContentRaw = + body?.editedMessage?.conversation ?? + body?.editedMessage?.extendedTextMessage?.text ?? + body?.editedMessage?.imageMessage?.caption ?? + body?.editedMessage?.videoMessage?.caption ?? + body?.editedMessage?.documentMessage?.caption ?? + (typeof body?.text === 'string' ? body.text : undefined); + + const editedMessageContent = (editedMessageContentRaw ?? '').trim(); + + // Sem conteúdo? Ignora aqui. O DELETE vai gerar o placeholder se for o caso. + if (!editedMessageContent) { + this.logger.info('[CW.EDIT] Conteúdo vazio — ignorando (DELETE tratará se for revoke).'); + return; + } + const message = await this.getMessageByKeyId(instance, body?.key?.id); if (!message) { this.logger.warn('Message not found for edit event'); return; } const key = message.key as WAMessageKey; - const messageType = key?.fromMe ? 'outgoing' : 'incoming'; - if (message && message.chatwootConversationId && message.chatwootMessageId) { - // Criar nova mensagem com formato: "Mensagem editada:\n\nteste1" - const editedText = `\n\n\`${i18next.t('cw.message.edited')}:\`\n\n${editedMessageContent}`; - + if (message.chatwootConversationId) { + const label = `\`${i18next.t('cw.message.edited')}\``; // "Mensagem editada" + const editedText = `${label}:${editedMessageContent}`; const send = await this.createMessage( instance, message.chatwootConversationId, @@ -2294,20 +2341,17 @@ export class ChatwootService { messageType, false, [], - { - message: { extendedTextMessage: { contextInfo: { stanzaId: key.id } } }, - }, + { message: { extendedTextMessage: { contextInfo: { stanzaId: key.id } } } }, 'WAID:' + body.key.id, null, ); - if (!send) { - this.logger.warn('edited message not sent'); - return; - } + if (!send) this.logger.warn('edited message not sent'); } return; } + // FIM DA EDIÇÃO + if (event === 'messages.read') { if (!body?.key?.id || !body?.key?.remoteJid) { this.logger.warn('message id not found'); @@ -2399,7 +2443,7 @@ export class ChatwootService { const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); const fileStream = new Readable(); - fileStream._read = () => {}; + fileStream._read = () => { }; fileStream.push(fileData); fileStream.push(null); @@ -2608,4 +2652,4 @@ export class ChatwootService { return; } } -} +} \ No newline at end of file diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index 27d170e8b..7069e472a 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -116,26 +116,23 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { logger.verbose( `Saving: remoteJid=${remoteJid}, jidOptions=${uniqueNumbers.join(',')}, lid=${item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null}`, ); - - if (existingRecord) { - await prismaRepository.isOnWhatsapp.update({ - where: { id: existingRecord.id }, - data: { - remoteJid: remoteJid, - jidOptions: uniqueNumbers.join(','), - lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null, - }, - }); - } else { - await prismaRepository.isOnWhatsapp.create({ - data: { - remoteJid: remoteJid, - jidOptions: uniqueNumbers.join(','), - lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null, - }, - }); - } - } + + await prismaRepository.isOnWhatsapp.upsert({ + where: { remoteJid: remoteJid }, // Prisma tenta encontrar o registro aqui + + update: { + // Campos de update + jidOptions: uniqueNumbers.join(','), + lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null, + }, // Se encontrar, ele atualiza + create: { + // Campos de criação + remoteJid: remoteJid, + jidOptions: uniqueNumbers.join(','), + lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null, + }, // Se NÃO encontrar, ele cria + }); + } } }