From 8c472cf6abc8f239973db5232eb301ddd0b1e41d Mon Sep 17 00:00:00 2001 From: Pedro Barbagli Date: Wed, 29 Jul 2026 17:23:06 +0000 Subject: [PATCH 1/4] fix: Chatwoot integration drops templateMessage, conversation never created getTypeMessage() has no case for templateMessage (WhatsApp Business template), so the first message from a contact using this type produces no body, eventWhatsapp logs 'no body message found' and returns before calling createConversation. The conversation only gets created later if a normal-type message follows. Extracts hydratedTemplate.hydratedContentText, mirroring how templateMessage is already handled in the getContentMessage() fallback a few lines below. Fixes the templateMessage item tracked in #2014. Duplicate of #2500. --- .../integrations/chatbot/chatwoot/services/chatwoot.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index c7a9753aa6..7423064dda 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1858,6 +1858,7 @@ export class ChatwootService { msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url, + templateMessage: msg.templateMessage?.hydratedTemplate?.hydratedContentText, }; return types; From 3df8dea702fef1b8044d02abb55716db6baf7e73 Mon Sep 17 00:00:00 2001 From: Pedro Barbagli Date: Wed, 29 Jul 2026 17:44:57 +0000 Subject: [PATCH 2/4] fix: add optional chaining on msg for templateMessage field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches the null-safety style of the surrounding fields in getTypeMessage() — msg could theoretically be undefined/null. Addresses sourcery-ai review comment on #2666. --- .../integrations/chatbot/chatwoot/services/chatwoot.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 7423064dda..d12db232a0 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1858,7 +1858,7 @@ export class ChatwootService { msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url, - templateMessage: msg.templateMessage?.hydratedTemplate?.hydratedContentText, + templateMessage: msg?.templateMessage?.hydratedTemplate?.hydratedContentText, }; return types; From 0b1d9b146873b394d498149fd298fd4cce4ad557 Mon Sep 17 00:00:00 2001 From: Pedro Barbagli Date: Thu, 30 Jul 2026 23:45:26 +0000 Subject: [PATCH 3/4] fix(chatwoot): stop silently dropping conversations for unrecognized WhatsApp message types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getTypeMessage() only recognizes a fixed list of message types; anything else produces no body, logs a bare "no body message found", and the conversation is silently never created. A user reported this on PR #2666 with production data (60-day window, 1:1 inbound): every unrecognized type dropped at 100% — templateMessage (37/37), secretEncryptedMessage (24/24), associatedChildMessage (13/13), buttonsMessage (12/12), albumMessage (11/11), interactiveMessage (7/7), placeholderMessage (3/3). Confirmed the same drop happening on this instance (a live secretEncryptedMessage silently dropped a few hours after redeploy). Looked up each type's actual shape in Baileys' WAProto.proto before deciding how to handle it, rather than a blanket fallback: - buttonsMessage: real content field (contentText) was never extracted - interactiveMessage: real content field (body.text) was never extracted - albumMessage: carries no text by design (WAProto: only expectedImageCount/expectedVideoCount) — synthesize an informative label instead of a text extractor that could never exist - associatedChildMessage: WAProto FutureProofMessage, just wraps a real embedded message (e.g. an album's individual photo) — unwrap recursively in getConversationMessage() instead of treating it as its own type - placeholderMessage: WhatsApp-internal only (MASK_LINKED_DEVICES) — never real user content, so explicitly skip rather than warn/drop - secretEncryptedMessage: encrypted edit/event payload targeting an existing message (SecretEncType: EVENT_EDIT/MESSAGE_EDIT), not new displayable content — decrypting and applying it as an edit isn't implemented, so explicitly skip with a clear reason instead of logging a misleading "no body" warning for what isn't a lost message For any other/future type not covered above, degrade instead of dropping: create the conversation with a "[mensagem não suportada: X]" placeholder body and log the actual type name, so a gap like this is visible from the logs instead of only discoverable by reconciling two databases by hand (as the reporting user had to do). --- .../chatwoot/services/chatwoot.service.ts | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index d12db232a0..3d124d6f7c 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1859,6 +1859,13 @@ export class ChatwootService { msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url, templateMessage: msg?.templateMessage?.hydratedTemplate?.hydratedContentText, + buttonsMessage: msg?.buttonsMessage?.contentText, + interactiveMessage: msg?.interactiveMessage?.body?.text, + // AlbumMessage carries no text (WAProto: only expectedImageCount/expectedVideoCount) — + // the photos/videos arrive as separate associatedChildMessage-wrapped messages. + albumMessage: msg?.albumMessage + ? `[álbum: ${msg.albumMessage.expectedImageCount ?? 0} foto(s), ${msg.albumMessage.expectedVideoCount ?? 0} vídeo(s)]` + : undefined, }; return types; @@ -2105,6 +2112,12 @@ export class ChatwootService { } public getConversationMessage(msg: any) { + // associatedChildMessage (WAProto: FutureProofMessage) just wraps a real embedded + // message (e.g. an album's individual photo/video) — unwrap and extract from that. + if (msg?.associatedChildMessage?.message) { + return this.getConversationMessage(msg.associatedChildMessage.message); + } + const types = this.getTypeMessage(msg); const messageContent = this.getMessageContent(types); @@ -2193,8 +2206,22 @@ export class ChatwootService { }; } + // WhatsApp-internal placeholder (e.g. device-linking mask) — never real user content. + if (body.message?.placeholderMessage) { + this.logger.verbose('Ignoring internal WhatsApp placeholderMessage (not a user message)'); + return; + } + + // Encrypted edit/event payload targeting an existing message (WAProto SecretEncryptedMessage: + // EVENT_EDIT/MESSAGE_EDIT) — not new displayable content. Decrypting and applying it as an + // edit isn't implemented; skip explicitly so it isn't logged as a dropped/lost message. + if (body.message?.secretEncryptedMessage) { + this.logger.verbose('Ignoring secretEncryptedMessage (message edit/event payload, not new content)'); + return; + } + const originalMessage = await this.getConversationMessage(body.message); - const bodyMessage = originalMessage + let bodyMessage = originalMessage ? originalMessage .replaceAll(/\*((?!\s)([^\n*]+?)(? body.message[key] !== undefined); + + if (!detectedType) { + this.logger.warn('no body message found (empty message payload)'); + return; + } + + this.logger.warn( + `no content extractor for message type "${detectedType}" — creating conversation with a placeholder body instead of dropping it`, + ); + bodyMessage = `[mensagem não suportada: ${detectedType}]`; } const getConversation = await this.createConversation(instance, body); From 1461aaf41b6b721ae391aed5829ce2996405a635 Mon Sep 17 00:00:00 2001 From: Pedro Barbagli Date: Fri, 31 Jul 2026 00:08:36 +0000 Subject: [PATCH 4/4] fix(chatwoot): extract content from pollCreationMessage and groupInviteMessage Two more common person-to-person message types with no content extractor in getTypeMessage(), so they'd have fallen into the new generic placeholder instead of showing real content: poll questions (pollCreationMessage.name) and group invite links (groupInviteMessage .caption, falling back to .groupName). --- .../integrations/chatbot/chatwoot/services/chatwoot.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 3d124d6f7c..e4bc65c263 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1866,6 +1866,8 @@ export class ChatwootService { albumMessage: msg?.albumMessage ? `[álbum: ${msg.albumMessage.expectedImageCount ?? 0} foto(s), ${msg.albumMessage.expectedVideoCount ?? 0} vídeo(s)]` : undefined, + pollCreationMessage: msg?.pollCreationMessage?.name, + groupInviteMessage: msg?.groupInviteMessage?.caption || msg?.groupInviteMessage?.groupName, }; return types;