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 c8734201..9b83e609 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 11f71b17..064a2a97 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 3b156c31..f6580848 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 68320367..03712bfd 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 f6580848..8d5b2dab 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 8d5b2dab..0ceaa3eb 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 9b83e609..32cd1ba3 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 32cd1ba3..e055b2bd 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 0ceaa3eb..906fff18 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 | 381 ++++++++++++------ 1 file changed, 260 insertions(+), 121 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index e055b2bd..a793c4d7 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); + 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.sendDataWebhook(Events.CALL, call); } - const msg = await this.client.sendMessage(call.from, { text: settings.msgCall }); - this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify' }); - } + if (events['connection.update']) { + this.connectionUpdate(events['connection.update']); + } - this.sendDataWebhook(Events.CALL, call); - } + if (events['creds.update']) { + this.instance.authState.saveCreds(); + } - if (events['connection.update']) { - this.connectionUpdate(events['connection.update']); - } + if (events['messaging-history.set']) { + const payload = events['messaging-history.set']; + await this.messageHandle['messaging-history.set'](payload); + } - if (events['creds.update']) { - this.instance.authState.saveCreds(); - } + if (events['messages.upsert']) { + const payload = events['messages.upsert']; - if (events['messaging-history.set']) { - const payload = events['messaging-history.set']; - this.messageHandle['messaging-history.set'](payload); - } + // this.messageProcessor.processMessage(payload, settings); + await this.messageHandle['messages.upsert'](payload, settings); + } - if (events['messages.upsert']) { - const payload = events['messages.upsert']; + if (events['messages.update']) { + const payload = events['messages.update']; + await this.messageHandle['messages.update'](payload, settings); + } - this.messageProcessor.processMessage(payload, settings); - // this.messageHandle['messages.upsert'](payload, settings); - } + if (events['message-receipt.update']) { + const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[]; + const remotesJidMap: Record = {}; - if (events['messages.update']) { - const payload = events['messages.update']; - this.messageHandle['messages.update'](payload, settings); - } + for (const event of payload) { + if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') { + remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp; + } + } - if (events['message-receipt.update']) { - const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[]; - const remotesJidMap: Record = {}; + await Promise.all( + Object.keys(remotesJidMap).map(async (remoteJid) => + this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]), + ), + ); + } - for (const event of payload) { - if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') { - remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp; + if (events['presence.update']) { + const payload = events['presence.update']; + + if (settings?.groupsIgnore && payload.id.includes('@g.us')) { + return; + } + + this.sendDataWebhook(Events.PRESENCE_UPDATE, 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['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.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['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[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; } } - - await Promise.all( - Object.keys(remotesJidMap).map(async (remoteJid) => - this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]), - ), - ); + } catch (error) { + this.logger.error(error); } - - if (events['presence.update']) { - const payload = events['presence.update']; - - if (settings?.groupsIgnore && payload.id.includes('@g.us')) { - return; - } - - this.sendDataWebhook(Events.PRESENCE_UPDATE, 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['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.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['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[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; - } - } + }); }); } 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 a793c4d7..62b6aa1c 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 62b6aa1c..b0df92f2 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 }); }