From baff4e8f5efbc57dbb7a5035748702e43f45f72f Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Fri, 28 Nov 2025 16:18:33 -0300 Subject: [PATCH 1/8] fix: update remoteJid handling to avoid unnecessary splitting for message number --- .../whatsapp/whatsapp.baileys.service.ts | 4 +- .../chatbot/base-chatbot.service.ts | 6 +- .../chatwoot/services/chatwoot.service.ts | 78 ++++++++++++++++++- .../typebot/services/typebot.service.ts | 10 +-- 4 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index c87342013..9b83e6095 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1614,9 +1614,9 @@ export class BaileysStartupService extends ChannelStartupService { // This enables LID to phoneNumber conversion without breaking existing webhook consumers // Helper to normalize participantId as phone number - const normalizePhoneNumber = (id: string): string => { + const normalizePhoneNumber = (id: string | any): string => { // Remove @lid, @s.whatsapp.net suffixes and extract just the number part - return id.split('@')[0]; + return String(id || '').split('@')[0]; }; try { diff --git a/src/api/integrations/chatbot/base-chatbot.service.ts b/src/api/integrations/chatbot/base-chatbot.service.ts index 11f71b17e..064a2a973 100644 --- a/src/api/integrations/chatbot/base-chatbot.service.ts +++ b/src/api/integrations/chatbot/base-chatbot.service.ts @@ -211,7 +211,7 @@ export abstract class BaseChatbotService { try { if (mediaType === 'audio') { await instance.audioWhatsapp({ - number: remoteJid.split('@')[0], + number: remoteJid, delay: (settings as any)?.delayMessage || 1000, audio: url, caption: altText, @@ -219,7 +219,7 @@ export abstract class BaseChatbotService { } else { await instance.mediaMessage( { - number: remoteJid.split('@')[0], + number: remoteJid, delay: (settings as any)?.delayMessage || 1000, mediatype: mediaType, media: url, @@ -290,7 +290,7 @@ export abstract class BaseChatbotService { setTimeout(async () => { await instance.textMessage( { - number: remoteJid.split('@')[0], + number: remoteJid, delay: settings?.delayMessage || 1000, text: message, linkPreview, diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 3b156c312..f6580848a 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -346,6 +346,20 @@ export class ChatwootService { return contact; } catch (error) { + if ( + (error.status === 422 || error.response?.status === 422) && + (error.message?.includes('taken') || error.response?.data?.message?.includes('taken')) && + jid + ) { + this.logger.warn(`Contact with identifier ${jid} already exists, trying to find it...`); + const existingContact = await this.findContactByIdentifier(instance, jid); + if (existingContact) { + const contactId = existingContact.id; + await this.addLabelToContact(this.provider.nameInbox, contactId); + return existingContact; + } + } + this.logger.error('Error creating contact'); console.log(error); return null; @@ -415,6 +429,55 @@ export class ChatwootService { } } + public async findContactByIdentifier(instance: InstanceDto, identifier: string) { + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + // Direct search by query (q) - most common way to search by identifier/email/phone + const contact = (await (client as any).get('contacts/search', { + params: { + q: identifier, + sort: 'name', + }, + })) as any; + + if (contact && contact.data && contact.data.payload && contact.data.payload.length > 0) { + return contact.data.payload[0]; + } + + // Fallback for older API versions or different response structures + if (contact && contact.payload && contact.payload.length > 0) { + return contact.payload[0]; + } + + // Try search by attribute + const contactByAttr = (await (client as any).post('contacts/filter', { + payload: [ + { + attribute_key: 'identifier', + filter_operator: 'equal_to', + values: [identifier], + query_operator: null, + }, + ], + })) as any; + + if (contactByAttr && contactByAttr.payload && contactByAttr.payload.length > 0) { + return contactByAttr.payload[0]; + } + + // Check inside data property if using axios interceptors wrapper + if (contactByAttr && contactByAttr.data && contactByAttr.data.payload && contactByAttr.data.payload.length > 0) { + return contactByAttr.data.payload[0]; + } + + return null; + } + public async findContact(instance: InstanceDto, phoneNumber: string) { const client = await this.clientCw(instance); @@ -1574,7 +1637,11 @@ export class ChatwootService { this.logger.verbose(`Update result: ${result} rows affected`); if (this.isImportHistoryAvailable()) { - chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id); + try { + await chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id); + } catch (error) { + this.logger.error(`Error updating Chatwoot message source ID: ${error}`); + } } } @@ -2024,7 +2091,7 @@ export class ChatwootService { if (body.key.remoteJid.includes('@g.us')) { const participantName = body.pushName; const rawPhoneNumber = - body.key.addressingMode === 'lid' && !body.key.fromMe + body.key.addressingMode === 'lid' && !body.key.fromMe && body.key.participantAlt ? body.key.participantAlt.split('@')[0].split(':')[0] : body.key.participant.split('@')[0].split(':')[0]; const formattedPhoneNumber = parsePhoneNumberFromString(`+${rawPhoneNumber}`).formatInternational(); @@ -2206,7 +2273,7 @@ export class ChatwootService { if (body.key.remoteJid.includes('@g.us')) { const participantName = body.pushName; const rawPhoneNumber = - body.key.addressingMode === 'lid' && !body.key.fromMe + body.key.addressingMode === 'lid' && !body.key.fromMe && body.key.participantAlt ? body.key.participantAlt.split('@')[0].split(':')[0] : body.key.participant.split('@')[0].split(':')[0]; const formattedPhoneNumber = parsePhoneNumberFromString(`+${rawPhoneNumber}`).formatInternational(); @@ -2465,7 +2532,10 @@ export class ChatwootService { } public getNumberFromRemoteJid(remoteJid: string) { - return remoteJid.replace(/:\d+/, '').split('@')[0]; + if (!remoteJid) { + return ''; + } + return remoteJid.replace(/:\d+/, '').replace('@s.whatsapp.net', '').replace('@g.us', '').replace('@lid', ''); } public startImportHistoryMessages(instance: InstanceDto) { diff --git a/src/api/integrations/chatbot/typebot/services/typebot.service.ts b/src/api/integrations/chatbot/typebot/services/typebot.service.ts index 683203675..03712bfdb 100644 --- a/src/api/integrations/chatbot/typebot/services/typebot.service.ts +++ b/src/api/integrations/chatbot/typebot/services/typebot.service.ts @@ -327,7 +327,7 @@ export class TypebotService extends BaseChatbotService { if (message.type === 'image') { await instance.mediaMessage( { - number: session.remoteJid.split('@')[0], + number: session.remoteJid, delay: settings?.delayMessage || 1000, mediatype: 'image', media: message.content.url, @@ -342,7 +342,7 @@ export class TypebotService extends BaseChatbotService { if (message.type === 'video') { await instance.mediaMessage( { - number: session.remoteJid.split('@')[0], + number: session.remoteJid, delay: settings?.delayMessage || 1000, mediatype: 'video', media: message.content.url, @@ -357,7 +357,7 @@ export class TypebotService extends BaseChatbotService { if (message.type === 'audio') { await instance.audioWhatsapp( { - number: session.remoteJid.split('@')[0], + number: session.remoteJid, delay: settings?.delayMessage || 1000, encoding: true, audio: message.content.url, @@ -441,7 +441,7 @@ export class TypebotService extends BaseChatbotService { */ private async processListMessage(instance: any, formattedText: string, remoteJid: string) { const listJson = { - number: remoteJid.split('@')[0], + number: remoteJid, title: '', description: '', buttonText: '', @@ -490,7 +490,7 @@ export class TypebotService extends BaseChatbotService { */ private async processButtonMessage(instance: any, formattedText: string, remoteJid: string) { const buttonJson = { - number: remoteJid.split('@')[0], + number: remoteJid, thumbnailUrl: undefined, title: '', description: '', From faed3f45746f181092fc9a745beea5744fdda2c7 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Fri, 28 Nov 2025 16:32:06 -0300 Subject: [PATCH 2/8] fix: improve error handling for existing contacts and simplify remoteJid processing --- .../chatbot/chatwoot/services/chatwoot.service.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index f6580848a..8d5b2dab3 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -346,12 +346,8 @@ export class ChatwootService { return contact; } catch (error) { - if ( - (error.status === 422 || error.response?.status === 422) && - (error.message?.includes('taken') || error.response?.data?.message?.includes('taken')) && - jid - ) { - this.logger.warn(`Contact with identifier ${jid} already exists, trying to find it...`); + if ((error.status === 422 || error.response?.status === 422) && jid) { + this.logger.warn(`Contact with identifier ${jid} creation failed (422). Checking if it already exists...`); const existingContact = await this.findContactByIdentifier(instance, jid); if (existingContact) { const contactId = existingContact.id; @@ -2535,7 +2531,7 @@ export class ChatwootService { if (!remoteJid) { return ''; } - return remoteJid.replace(/:\d+/, '').replace('@s.whatsapp.net', '').replace('@g.us', '').replace('@lid', ''); + return remoteJid.replace(/:\d+/, '').split('@')[0]; } public startImportHistoryMessages(instance: InstanceDto) { From 92c2ace7bcec3378b85303702ed8ebb1d5903d3c Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Fri, 28 Nov 2025 19:03:24 -0300 Subject: [PATCH 3/8] fix: enhance remoteJid processing to handle '@lid' cases --- .../integrations/chatbot/chatwoot/services/chatwoot.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 8d5b2dab3..0ceaa3eb8 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -2531,6 +2531,9 @@ export class ChatwootService { if (!remoteJid) { return ''; } + if (remoteJid.includes('@lid')) { + return remoteJid; + } return remoteJid.replace(/:\d+/, '').split('@')[0]; } From bee309cd28ff0fbd75d51bb9fa50bf4c6d4ce060 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Fri, 28 Nov 2025 21:14:19 -0300 Subject: [PATCH 4/8] fix: streamline message handling logic and improve cache management in BaileysStartupService --- .../channel/whatsapp/whatsapp.baileys.service.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 9b83e6095..32cd1ba33 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1148,12 +1148,7 @@ export class BaileysStartupService extends ChannelStartupService { } } - if ( - (type !== 'notify' && type !== 'append') || - editedMessage || - received.message?.pollUpdateMessage || - !received?.message - ) { + if ((type !== 'notify' && type !== 'append') || editedMessage || !received?.message) { continue; } @@ -1447,18 +1442,18 @@ export class BaileysStartupService extends ChannelStartupService { continue; } - if (update.message !== null && update.status === undefined) continue; + if (update.message === null && update.status === undefined) continue; const updateKey = `${this.instance.id}_${key.id}_${update.status}`; const cached = await this.baileysCache.get(updateKey); - if (cached) { + if (cached && update.messageTimestamp == cached.messageTimestamp) { this.logger.info(`Update Message duplicated ignored [avoid deadlock]: ${updateKey}`); continue; } - await this.baileysCache.set(updateKey, true, 30 * 60); + await this.baileysCache.set(updateKey, update.messageTimestamp, 30 * 60); if (status[update.status] === 'READ' && key.fromMe) { if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { @@ -1489,7 +1484,7 @@ export class BaileysStartupService extends ChannelStartupService { remoteJid: key?.remoteJid, fromMe: key.fromMe, participant: key?.participant, - status: status[update.status] ?? 'DELETED', + status: status[update.status] ?? 'SERVER_ACK', pollUpdates, instanceId: this.instanceId, }; From 250ddd2e89082c7a77c612e923b5cb68b7644cde Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Fri, 28 Nov 2025 21:28:45 -0300 Subject: [PATCH 5/8] fix(chatwoot): improve jid normalization and type safety in chatwoot integration Refactor to preserve LID identifiers and update parameter type for better type safety as per code review feedback. --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 2 +- .../integrations/chatbot/chatwoot/services/chatwoot.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 32cd1ba33..e055b2bd7 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1609,7 +1609,7 @@ export class BaileysStartupService extends ChannelStartupService { // This enables LID to phoneNumber conversion without breaking existing webhook consumers // Helper to normalize participantId as phone number - const normalizePhoneNumber = (id: string | any): string => { + const normalizePhoneNumber = (id: string | null | undefined): string => { // Remove @lid, @s.whatsapp.net suffixes and extract just the number part return String(id || '').split('@')[0]; }; diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 0ceaa3eb8..906fff188 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -2527,7 +2527,7 @@ export class ChatwootService { } } - public getNumberFromRemoteJid(remoteJid: string) { + public normalizeJidIdentifier(remoteJid: string) { if (!remoteJid) { return ''; } From 2408384b0fbe7d38c230d2396d3e2c7eff62af89 Mon Sep 17 00:00:00 2001 From: Vitor Manoel Santos Moura <72520858+Vitordotpy@users.noreply.github.com> Date: Sun, 30 Nov 2025 00:25:17 -0300 Subject: [PATCH 6/8] Refactor message handling and polling updates Refactor message handling and polling updates, including decryption logic for poll votes and cache management for message updates. Improved event processing flow and added handling for various message types. --- .../whatsapp/whatsapp.baileys.service.ts | 359 ++++++++++++------ 1 file changed, 249 insertions(+), 110 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index e055b2bd7..a793c4d70 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -90,6 +90,7 @@ import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; import axios from 'axios'; +import { createHash } from 'crypto'; import makeWASocket, { AnyMessageContent, BufferedEventData, @@ -100,9 +101,11 @@ import makeWASocket, { ConnectionState, Contact, delay, + decryptPollVote, DisconnectReason, downloadContentFromMessage, downloadMediaMessage, + jidNormalizedUser, generateWAMessageFromContent, getAggregateVotesInPollMessage, GetCatalogOptions, @@ -247,6 +250,7 @@ export class BaileysStartupService extends ChannelStartupService { private readonly userDevicesCache: CacheStore = new NodeCache({ stdTTL: 300000, useClones: false }); private endSession = false; private logBaileys = this.configService.get('LOG').BAILEYS; + private eventProcessingQueue: Promise = Promise.resolve(); // Cache TTL constants (in seconds) private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing @@ -1121,6 +1125,11 @@ export class BaileysStartupService extends ChannelStartupService { ); await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage); + + if (received.key?.id && editedMessage.key?.id) { + await this.baileysCache.set(`protocol_${received.key.id}`, editedMessage.key.id, 60 * 60 * 24); + } + const oldMessage = await this.getMessage(editedMessage.key, true); if ((oldMessage as any)?.id) { const editedMessageTimestamp = Long.isLong(received?.messageTimestamp) @@ -1188,6 +1197,109 @@ export class BaileysStartupService extends ChannelStartupService { const messageRaw = this.prepareMessage(received); + if (messageRaw.messageType === 'pollUpdateMessage') { + const pollCreationKey = messageRaw.message.pollUpdateMessage.pollCreationMessageKey; + const pollMessage = (await this.getMessage(pollCreationKey, true)) as proto.IWebMessageInfo; + const pollMessageSecret = (await this.getMessage(pollCreationKey)) as any; + + if (pollMessage) { + const pollOptions = + (pollMessage.message as any).pollCreationMessage?.options || + (pollMessage.message as any).pollCreationMessageV3?.options || + []; + const pollVote = messageRaw.message.pollUpdateMessage.vote; + + const voterJid = received.key.fromMe + ? this.instance.wuid + : received.key.participant || received.key.remoteJid; + + let pollEncKey = pollMessageSecret?.messageContextInfo?.messageSecret; + + let successfulVoterJid = voterJid; + + if (typeof pollEncKey === 'string') { + pollEncKey = Buffer.from(pollEncKey, 'base64'); + } else if (pollEncKey?.type === 'Buffer' && Array.isArray(pollEncKey.data)) { + pollEncKey = Buffer.from(pollEncKey.data); + } + + if (Buffer.isBuffer(pollEncKey) && pollEncKey.length === 44) { + pollEncKey = Buffer.from(pollEncKey.toString('utf8'), 'base64'); + } + + if (pollVote.encPayload && pollEncKey) { + const creatorCandidates = [ + this.instance.wuid, + this.client.user?.lid, + pollMessage.key.participant, + (pollMessage.key as any).participantAlt, + pollMessage.key.remoteJid, + ]; + + const key = received.key as any; + const voterCandidates = [ + this.instance.wuid, + this.client.user?.lid, + key.participant, + key.participantAlt, + key.remoteJidAlt, + key.remoteJid, + ]; + + const uniqueCreators = [ + ...new Set(creatorCandidates.filter(Boolean).map((id) => jidNormalizedUser(id))), + ]; + const uniqueVoters = [ + ...new Set(voterCandidates.filter(Boolean).map((id) => jidNormalizedUser(id))), + ]; + + let decryptedVote; + + for (const creator of uniqueCreators) { + for (const voter of uniqueVoters) { + try { + decryptedVote = decryptPollVote(pollVote, { + pollCreatorJid: creator, + pollMsgId: pollMessage.key.id, + pollEncKey, + voterJid: voter, + } as any); + if (decryptedVote) { + successfulVoterJid = voter; + break; + } + } catch (err) { + // Continue trying + } + } + if (decryptedVote) break; + } + + if (decryptedVote) { + Object.assign(pollVote, decryptedVote); + } + } + + const selectedOptions = pollVote?.selectedOptions || []; + + const selectedOptionNames = pollOptions + .filter((option) => { + const hash = createHash('sha256').update(option.optionName).digest(); + return selectedOptions.some((selected) => Buffer.compare(selected, hash) === 0); + }) + .map((option) => option.optionName); + + messageRaw.message.pollUpdateMessage.vote.selectedOptions = selectedOptionNames; + + const pollUpdates = pollOptions.map((option) => ({ + name: option.optionName, + voters: selectedOptionNames.includes(option.optionName) ? [successfulVoterJid] : [], + })); + + messageRaw.pollUpdates = pollUpdates; + } + } + const isMedia = received?.message?.imageMessage || received?.message?.videoMessage || @@ -1237,7 +1349,8 @@ export class BaileysStartupService extends ChannelStartupService { } if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { - const msg = await this.prismaRepository.message.create({ data: messageRaw }); + const { pollUpdates, ...messageData } = messageRaw; + const msg = await this.prismaRepository.message.create({ data: messageData }); const { remoteJid } = received.key; const timestamp = msg.messageTimestamp; @@ -1442,18 +1555,23 @@ export class BaileysStartupService extends ChannelStartupService { continue; } - if (update.message === null && update.status === undefined) continue; - const updateKey = `${this.instance.id}_${key.id}_${update.status}`; const cached = await this.baileysCache.get(updateKey); - if (cached && update.messageTimestamp == cached.messageTimestamp) { + const secondsSinceEpoch = Math.floor(Date.now() / 1000) + console.log('CACHE:', {cached, updateKey, messageTimestamp: update.messageTimestamp, secondsSinceEpoch}); + + if ((update.messageTimestamp && update.messageTimestamp === cached) || (!update.messageTimestamp && secondsSinceEpoch === cached)) { this.logger.info(`Update Message duplicated ignored [avoid deadlock]: ${updateKey}`); continue; } - await this.baileysCache.set(updateKey, update.messageTimestamp, 30 * 60); + if (update.messageTimestamp) { + await this.baileysCache.set(updateKey, update.messageTimestamp, 30 * 60); + } else { + await this.baileysCache.set(updateKey, secondsSinceEpoch, 30 * 60); + } if (status[update.status] === 'READ' && key.fromMe) { if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { @@ -1489,14 +1607,27 @@ export class BaileysStartupService extends ChannelStartupService { instanceId: this.instanceId, }; + if (update.message) { + message.message = update.message; + } + let findMessage: any; const configDatabaseData = this.configService.get('DATABASE').SAVE_DATA; if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) { // Use raw SQL to avoid JSON path issues + const protocolMapKey = `protocol_${key.id}`; + const originalMessageId = (await this.baileysCache.get(protocolMapKey)) as string; + + if (originalMessageId) { + message.keyId = originalMessageId; + } + + const searchId = originalMessageId || key.id; + const messages = (await this.prismaRepository.$queryRaw` SELECT * FROM "Message" WHERE "instanceId" = ${this.instanceId} - AND "key"->>'id' = ${key.id} + AND "key"->>'id' = ${searchId} LIMIT 1 `) as any[]; findMessage = messages[0] || null; @@ -1509,7 +1640,7 @@ export class BaileysStartupService extends ChannelStartupService { } if (update.message === null && update.status === undefined) { - this.sendDataWebhook(Events.MESSAGES_DELETE, key); + this.sendDataWebhook(Events.MESSAGES_DELETE, { ...key, status: 'DELETED' }); if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) await this.prismaRepository.messageUpdate.create({ data: message }); @@ -1557,8 +1688,10 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.MESSAGES_UPDATE, message); - 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) { + const { message: _msg, ...messageData } = message; + await this.prismaRepository.messageUpdate.create({ data: messageData }); + } const existingChat = await this.prismaRepository.chat.findFirst({ where: { instanceId: this.instanceId, remoteJid: message.remoteJid }, @@ -1727,135 +1860,141 @@ export class BaileysStartupService extends ChannelStartupService { private eventHandler() { this.client.ev.process(async (events) => { - if (!this.endSession) { - const database = this.configService.get('DATABASE'); - const settings = await this.findSettings(); + this.eventProcessingQueue = this.eventProcessingQueue.then(async () => { + try { + if (!this.endSession) { + const database = this.configService.get('DATABASE'); + const settings = await this.findSettings(); - if (events.call) { - const call = events.call[0]; + if (events.call) { + const call = events.call[0]; - if (settings?.rejectCall && call.status == 'offer') { - this.client.rejectCall(call.id, call.from); - } + if (settings?.rejectCall && call.status == 'offer') { + this.client.rejectCall(call.id, call.from); + } - if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') { - if (call.from.endsWith('@lid')) { - call.from = await this.client.signalRepository.lidMapping.getPNForLID(call.from as string); - } - const msg = await this.client.sendMessage(call.from, { text: settings.msgCall }); + if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') { + if (call.from.endsWith('@lid')) { + call.from = await this.client.signalRepository.lidMapping.getPNForLID(call.from as string); + } + const msg = await this.client.sendMessage(call.from, { text: settings.msgCall }); - 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); - } + this.sendDataWebhook(Events.CALL, call); + } - if (events['connection.update']) { - this.connectionUpdate(events['connection.update']); - } + if (events['connection.update']) { + this.connectionUpdate(events['connection.update']); + } - if (events['creds.update']) { - this.instance.authState.saveCreds(); - } + if (events['creds.update']) { + this.instance.authState.saveCreds(); + } - if (events['messaging-history.set']) { - const payload = events['messaging-history.set']; - this.messageHandle['messaging-history.set'](payload); - } + if (events['messaging-history.set']) { + const payload = events['messaging-history.set']; + await this.messageHandle['messaging-history.set'](payload); + } - if (events['messages.upsert']) { - const payload = events['messages.upsert']; + if (events['messages.upsert']) { + const payload = events['messages.upsert']; - this.messageProcessor.processMessage(payload, settings); - // this.messageHandle['messages.upsert'](payload, settings); - } + // this.messageProcessor.processMessage(payload, settings); + await this.messageHandle['messages.upsert'](payload, settings); + } - if (events['messages.update']) { - const payload = events['messages.update']; - this.messageHandle['messages.update'](payload, settings); - } + if (events['messages.update']) { + const payload = events['messages.update']; + await this.messageHandle['messages.update'](payload, settings); + } - if (events['message-receipt.update']) { - const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[]; - const remotesJidMap: Record = {}; + if (events['message-receipt.update']) { + 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') { - remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp; - } - } + for (const event of payload) { + 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]), - ), - ); - } + await Promise.all( + Object.keys(remotesJidMap).map(async (remoteJid) => + this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]), + ), + ); + } - if (events['presence.update']) { - const payload = events['presence.update']; + if (events['presence.update']) { + const payload = events['presence.update']; - if (settings?.groupsIgnore && payload.id.includes('@g.us')) { - return; - } + if (settings?.groupsIgnore && payload.id.includes('@g.us')) { + return; + } - this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); - } + this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); + } - if (!settings?.groupsIgnore) { - if (events['groups.upsert']) { - const payload = events['groups.upsert']; - this.groupHandler['groups.upsert'](payload); - } + if (!settings?.groupsIgnore) { + if (events['groups.upsert']) { + const payload = events['groups.upsert']; + this.groupHandler['groups.upsert'](payload); + } - if (events['groups.update']) { - const payload = events['groups.update']; - this.groupHandler['groups.update'](payload); - } + if (events['groups.update']) { + const payload = events['groups.update']; + this.groupHandler['groups.update'](payload); + } - if (events['group-participants.update']) { - const payload = events['group-participants.update'] as any; - this.groupHandler['group-participants.update'](payload); - } - } + if (events['group-participants.update']) { + const payload = events['group-participants.update'] as any; + this.groupHandler['group-participants.update'](payload); + } + } - if (events['chats.upsert']) { - const payload = events['chats.upsert']; - this.chatHandle['chats.upsert'](payload); - } + if (events['chats.upsert']) { + const payload = events['chats.upsert']; + this.chatHandle['chats.upsert'](payload); + } - if (events['chats.update']) { - const payload = events['chats.update']; - this.chatHandle['chats.update'](payload); - } + if (events['chats.update']) { + const payload = events['chats.update']; + this.chatHandle['chats.update'](payload); + } - if (events['chats.delete']) { - const payload = events['chats.delete']; - this.chatHandle['chats.delete'](payload); - } + if (events['chats.delete']) { + const payload = events['chats.delete']; + this.chatHandle['chats.delete'](payload); + } - if (events['contacts.upsert']) { - const payload = events['contacts.upsert']; - this.contactHandle['contacts.upsert'](payload); - } + if (events['contacts.upsert']) { + const payload = events['contacts.upsert']; + this.contactHandle['contacts.upsert'](payload); + } - if (events['contacts.update']) { - const payload = events['contacts.update']; - this.contactHandle['contacts.update'](payload); - } + if (events['contacts.update']) { + const payload = events['contacts.update']; + this.contactHandle['contacts.update'](payload); + } - if (events[Events.LABELS_ASSOCIATION]) { - const payload = events[Events.LABELS_ASSOCIATION]; - this.labelHandle[Events.LABELS_ASSOCIATION](payload, database); - return; - } + if (events[Events.LABELS_ASSOCIATION]) { + const payload = events[Events.LABELS_ASSOCIATION]; + this.labelHandle[Events.LABELS_ASSOCIATION](payload, database); + return; + } - if (events[Events.LABELS_EDIT]) { - const payload = events[Events.LABELS_EDIT]; - this.labelHandle[Events.LABELS_EDIT](payload); - return; + if (events[Events.LABELS_EDIT]) { + const payload = events[Events.LABELS_EDIT]; + this.labelHandle[Events.LABELS_EDIT](payload); + return; + } + } + } catch (error) { + this.logger.error(error); } - } + }); }); } From bbf60e30b045ecfa2b92833fcd890d8c3c23c210 Mon Sep 17 00:00:00 2001 From: Vitor Manoel Santos Moura <72520858+Vitordotpy@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:51:34 -0300 Subject: [PATCH 7/8] Refactor imports and clean up code structure --- .../whatsapp/whatsapp.baileys.service.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index a793c4d70..62b6aa1cc 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -76,21 +76,19 @@ import { S3, } from '@config/env.config'; 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 { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { createJid } from '@utils/createJid'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; -import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; +import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; import { status } from '@utils/renderStatus'; import { sendTelemetry } from '@utils/sendTelemetry'; import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; -import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; -import axios from 'axios'; -import { createHash } from 'crypto'; + +import { BaileysMessageProcessor } from './baileysMessage.processor'; +import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; + import makeWASocket, { AnyMessageContent, BufferedEventData, @@ -105,7 +103,6 @@ import makeWASocket, { DisconnectReason, downloadContentFromMessage, downloadMediaMessage, - jidNormalizedUser, generateWAMessageFromContent, getAggregateVotesInPollMessage, GetCatalogOptions, @@ -116,6 +113,7 @@ import makeWASocket, { isJidGroup, isJidNewsletter, isPnUser, + jidNormalizedUser, makeCacheableSignalKeyStore, MessageUpsertType, MessageUserReceiptUpdate, @@ -134,15 +132,20 @@ import makeWASocket, { } from 'baileys'; import { Label } from 'baileys/lib/Types/Label'; import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation'; -import { spawn } from 'child_process'; +import { createId as cuid } from '@paralleldrive/cuid2'; +import { Instance, Message } from '@prisma/client'; +import axios from 'axios'; import { isArray, isBase64, isURL } from 'class-validator'; +import { createHash } from 'crypto'; import EventEmitter2 from 'eventemitter2'; import ffmpeg from 'fluent-ffmpeg'; +import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import FormData from 'form-data'; +import { Boom } from '@hapi/boom'; import Long from 'long'; import mimeTypes from 'mime-types'; -import NodeCache from 'node-cache'; import cron from 'node-cron'; +import NodeCache from 'node-cache'; import { release } from 'os'; import { join } from 'path'; import P from 'pino'; @@ -150,11 +153,9 @@ import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; import qrcodeTerminal from 'qrcode-terminal'; import sharp from 'sharp'; import { PassThrough, Readable } from 'stream'; +import { spawn } from 'child_process'; import { v4 } from 'uuid'; -import { BaileysMessageProcessor } from './baileysMessage.processor'; -import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; - export interface ExtendedIMessageKey extends proto.IMessageKey { remoteJidAlt?: string; participantAlt?: string; @@ -1250,7 +1251,7 @@ export class BaileysStartupService extends ChannelStartupService { ...new Set(creatorCandidates.filter(Boolean).map((id) => jidNormalizedUser(id))), ]; const uniqueVoters = [ - ...new Set(voterCandidates.filter(Boolean).map((id) => jidNormalizedUser(id))), + ...new Set(voterCandidates.filter(Boolean).map((id) => jidNormalizedUser(id))) ]; let decryptedVote; @@ -1268,7 +1269,7 @@ export class BaileysStartupService extends ChannelStartupService { successfulVoterJid = voter; break; } - } catch (err) { + } catch (_err) { // Continue trying } } @@ -1349,7 +1350,7 @@ export class BaileysStartupService extends ChannelStartupService { } if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { - const { pollUpdates, ...messageData } = messageRaw; + const { _pollUpdates, ...messageData } = messageRaw; const msg = await this.prismaRepository.message.create({ data: messageData }); const { remoteJid } = received.key; @@ -1559,10 +1560,12 @@ export class BaileysStartupService extends ChannelStartupService { const cached = await this.baileysCache.get(updateKey); - const secondsSinceEpoch = Math.floor(Date.now() / 1000) - console.log('CACHE:', {cached, updateKey, messageTimestamp: update.messageTimestamp, secondsSinceEpoch}); + const secondsSinceEpoch = Math.floor(Date.now() / 1000); - if ((update.messageTimestamp && update.messageTimestamp === cached) || (!update.messageTimestamp && secondsSinceEpoch === cached)) { + if ( + (update.messageTimestamp && update.messageTimestamp === cached) || + (!update.messageTimestamp && secondsSinceEpoch === cached) + ) { this.logger.info(`Update Message duplicated ignored [avoid deadlock]: ${updateKey}`); continue; } @@ -1689,7 +1692,7 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.MESSAGES_UPDATE, message); if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) { - const { message: _msg, ...messageData } = message; + const { message: __msg, ...messageData } = message; await this.prismaRepository.messageUpdate.create({ data: messageData }); } From c7a2aa51eeda32ac2e854fbb859f5fffa5ff7a24 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Sun, 30 Nov 2025 19:56:03 -0300 Subject: [PATCH 8/8] fix: reorganize imports and improve message handling in BaileysStartupService --- .../whatsapp/whatsapp.baileys.service.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 62b6aa1cc..b0df92f25 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -76,19 +76,20 @@ import { S3, } from '@config/env.config'; import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions'; -import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; +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 { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; +import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import { status } from '@utils/renderStatus'; import { sendTelemetry } from '@utils/sendTelemetry'; import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; +import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; - -import { BaileysMessageProcessor } from './baileysMessage.processor'; -import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; - +import axios from 'axios'; import makeWASocket, { AnyMessageContent, BufferedEventData, @@ -98,8 +99,8 @@ import makeWASocket, { Chat, ConnectionState, Contact, - delay, decryptPollVote, + delay, DisconnectReason, downloadContentFromMessage, downloadMediaMessage, @@ -132,20 +133,16 @@ import makeWASocket, { } from 'baileys'; import { Label } from 'baileys/lib/Types/Label'; import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation'; -import { createId as cuid } from '@paralleldrive/cuid2'; -import { Instance, Message } from '@prisma/client'; -import axios from 'axios'; +import { spawn } from 'child_process'; import { isArray, isBase64, isURL } from 'class-validator'; import { createHash } from 'crypto'; import EventEmitter2 from 'eventemitter2'; import ffmpeg from 'fluent-ffmpeg'; -import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import FormData from 'form-data'; -import { Boom } from '@hapi/boom'; import Long from 'long'; import mimeTypes from 'mime-types'; -import cron from 'node-cron'; import NodeCache from 'node-cache'; +import cron from 'node-cron'; import { release } from 'os'; import { join } from 'path'; import P from 'pino'; @@ -153,9 +150,11 @@ import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; import qrcodeTerminal from 'qrcode-terminal'; import sharp from 'sharp'; import { PassThrough, Readable } from 'stream'; -import { spawn } from 'child_process'; import { v4 } from 'uuid'; +import { BaileysMessageProcessor } from './baileysMessage.processor'; +import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; + export interface ExtendedIMessageKey extends proto.IMessageKey { remoteJidAlt?: string; participantAlt?: string; @@ -1250,9 +1249,7 @@ export class BaileysStartupService extends ChannelStartupService { const uniqueCreators = [ ...new Set(creatorCandidates.filter(Boolean).map((id) => jidNormalizedUser(id))), ]; - const uniqueVoters = [ - ...new Set(voterCandidates.filter(Boolean).map((id) => jidNormalizedUser(id))) - ]; + const uniqueVoters = [...new Set(voterCandidates.filter(Boolean).map((id) => jidNormalizedUser(id)))]; let decryptedVote; @@ -1269,7 +1266,7 @@ export class BaileysStartupService extends ChannelStartupService { successfulVoterJid = voter; break; } - } catch (_err) { + } catch { // Continue trying } } @@ -1350,7 +1347,8 @@ export class BaileysStartupService extends ChannelStartupService { } if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { - const { _pollUpdates, ...messageData } = messageRaw; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { pollUpdates, ...messageData } = messageRaw; const msg = await this.prismaRepository.message.create({ data: messageData }); const { remoteJid } = received.key; @@ -1561,6 +1559,7 @@ export class BaileysStartupService extends ChannelStartupService { const cached = await this.baileysCache.get(updateKey); const secondsSinceEpoch = Math.floor(Date.now() / 1000); + console.log('CACHE:', { cached, updateKey, messageTimestamp: update.messageTimestamp, secondsSinceEpoch }); if ( (update.messageTimestamp && update.messageTimestamp === cached) || @@ -1692,7 +1691,8 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.MESSAGES_UPDATE, message); if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) { - const { message: __msg, ...messageData } = message; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { message: _msg, ...messageData } = message; await this.prismaRepository.messageUpdate.create({ data: messageData }); }