externalAdReply (Click-to-WhatsApp ads) no longer reaches the Chatwoot integration on 2.4

Open
#2,721 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
74/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
typescript
Domain
api, backend

Research direction

Start in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts and inspect prepareMessage(), especially how contentMsg.contextInfo and quotedMessage are populated. Then read getAdsMessage() in src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts and compare the 2.3.7 and 2.4.0-rc2 behavior. Done means ad title, body, thumbnail and source URL reach Chatwoot, while plain text and quoted media context still work.

Written by the indexing model from the issue text.

Description

Summary

Since 2.4, messages coming from Instagram/Facebook click-to-WhatsApp ads arrive in
Chatwoot as plain text. The ad card — thumbnail, title, body, source URL — is
silently dropped. On 2.3.7 the same messages produced an attachment with the
resized thumbnail plus the ad title and link.

There is no error in the logs. The message itself is delivered; only the ad
context disappears.

Root cause

prepareMessage() in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
changed which field feeds messageRaw.contextInfo.

main (and 2.3.7):

const contentType = getContentType(message.message);
const contentMsg = message?.message[contentType] as any;
// ...
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),

develop (and 2.4.0-rc2):

contextInfo: this.deserializeMessageBuffers(message.message?.messageContextInfo),

messageContextInfo is the WhatsApp device-metadata block (deviceListMetadata,
E2E key info). It never carries externalAdReply. The ad context lives in the
content message's own contextInfo, e.g.
message.message.extendedTextMessage.contextInfo.externalAdReply.

Why the Chatwoot side then fails

getAdsMessage() in
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts is called as
this.getAdsMessage(body), where body is the message wrapper:

title: msg.extendedTextMessage?.contextInfo?.externalAdReply?.title
    || msg.contextInfo?.externalAdReply?.title,

The first branch never matches, because on the wrapper the content lives under
msg.message.extendedTextMessage, not msg.extendedTextMessage. So the whole
feature depended on the hoisted msg.contextInfo — precisely what the change
above repointed. getAdsMessage() now returns an object whose fields are all
undefined, the if (adsMessage && adsMessage.title || ...) branch is skipped,
and no attachment is created.

Side effect beyond ads

messageRaw?.contextInfo?.quotedMessage (same file) reads the same field, so
quote/reply context on media messages is lost as well.

Steps to reproduce

  1. Run 2.4.0-rc2 with the Chatwoot integration enabled.
  2. Publish an Instagram ad with a click-to-WhatsApp button.
  3. Click the ad and send any text.
  4. Chatwoot receives the text only. On 2.3.7 the same flow produced an
    attachment with the 320x180 thumbnail, the ad title and the source URL.

Suggested fix

Restore the previous source in prepareMessage():

const contentType = getContentType(message.message);
const contentMsg = message?.message[contentType] as any;
// ...
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),

Independently, getAdsMessage() would be more robust reading
msg.message?.extendedTextMessage?.contextInfo?.externalAdReply as its first
branch, so it no longer depends on the hoisted field at all.

Environment

  • Evolution API 2.4.0-rc2 (Docker, evoapicloud/evolution-api:2.4.0-rc2)
  • Chatwoot 4.17.1, self-hosted
  • Baileys channel, four instances
  • Regression confirmed by source diff between tags 2.3.7 and 2.4.0-rc2, and
    between branches main and develop.

Workaround in use

While this is open, we patch the compiled bundles
(dist/api/integrations/channel/whatsapp/whatsapp.baileys.service.{js,mjs}) in a
derived image, replacing the expression with one that does not depend on
minified symbol names:

contextInfo: this.deserializeMessageBuffers(
  (e.message && Object.values(e.message).find(v => v && typeof v === "object" && v.contextInfo)?.contextInfo)
  ?? e.message?.messageContextInfo
)

Checked against simulated payloads: ad leads recover title / body /
thumbnailUrl / sourceUrl, plain text messages are unaffected, and quoted
media regains its stanzaId. It is running in our production instance; we have
not yet observed a live ad lead through it.


Versão em português (pt-BR) — clique para expandir

Resumo

Desde a 2.4, mensagens vindas de anúncios click-to-WhatsApp do Instagram e do
Facebook chegam ao Chatwoot como texto puro. O cartão do anúncio — miniatura,
título, corpo e URL de origem — é descartado silenciosamente. Na 2.3.7 as mesmas
mensagens produziam um anexo com a miniatura redimensionada, mais o título e o
link do anúncio.

Não há erro no log. A mensagem em si é entregue; só o contexto do anúncio some.

Causa

O método prepareMessage(), em
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts, passou a
preencher messageRaw.contextInfo a partir de outro campo.

main (e 2.3.7):

const contentType = getContentType(message.message);
const contentMsg = message?.message[contentType] as any;
// ...
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),

develop (e 2.4.0-rc2):

contextInfo: this.deserializeMessageBuffers(message.message?.messageContextInfo),

messageContextInfo é o bloco de metadados de dispositivo do WhatsApp
(deviceListMetadata, informação de chaves E2E). Ele nunca carrega
externalAdReply. O contexto do anúncio vive na contextInfo da própria
mensagem de conteúdo, por exemplo
message.message.extendedTextMessage.contextInfo.externalAdReply.

Por que o lado do Chatwoot falha em seguida

getAdsMessage(), em
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts, é chamado
como this.getAdsMessage(body), onde body é o invólucro da mensagem:

title: msg.extendedTextMessage?.contextInfo?.externalAdReply?.title
    || msg.contextInfo?.externalAdReply?.title,

O primeiro ramo nunca casa, porque no invólucro o conteúdo fica em
msg.message.extendedTextMessage, e não em msg.extendedTextMessage. Ou seja,
o recurso inteiro dependia do msg.contextInfo hoisted — exatamente o que a
mudança acima repontou. getAdsMessage() agora devolve um objeto com todos os
campos undefined, o ramo if (adsMessage && adsMessage.title || ...) é pulado,
e nenhum anexo é criado.

Efeito colateral além dos anúncios

messageRaw?.contextInfo?.quotedMessage (mesmo arquivo) lê o mesmo campo, então
o contexto de citação/resposta em mensagens de mídia também se perde.

Como reproduzir

  1. Rodar a 2.4.0-rc2 com a integração do Chatwoot habilitada.
  2. Publicar um anúncio no Instagram com botão click-to-WhatsApp.
  3. Clicar no anúncio e enviar qualquer texto.
  4. O Chatwoot recebe apenas o texto. Na 2.3.7 o mesmo fluxo produzia um anexo
    com a miniatura 320x180, o título do anúncio e a URL de origem.

Correção sugerida

Restaurar a origem anterior em prepareMessage():

const contentType = getContentType(message.message);
const contentMsg = message?.message[contentType] as any;
// ...
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),

Independentemente disso, getAdsMessage() ficaria mais robusto lendo
msg.message?.extendedTextMessage?.contextInfo?.externalAdReply como primeiro
ramo, deixando de depender do campo hoisted.

Ambiente

  • Evolution API 2.4.0-rc2 (Docker, evoapicloud/evolution-api:2.4.0-rc2)
  • Chatwoot 4.17.1, self-hosted
  • Canal Baileys, quatro instâncias
  • Regressão confirmada por diff de fonte entre as tags 2.3.7 e 2.4.0-rc2, e
    entre os branches main e develop.

Contorno em uso

Enquanto isso não é corrigido, aplicamos um patch nos bundles compilados
(dist/api/integrations/channel/whatsapp/whatsapp.baileys.service.{js,mjs}) numa
imagem derivada, trocando a expressão por uma que não depende de nomes de
símbolos minificados:

contextInfo: this.deserializeMessageBuffers(
  (e.message && Object.values(e.message).find(v => v && typeof v === "object" && v.contextInfo)?.contextInfo)
  ?? e.message?.messageContextInfo
)

Verificado com payloads simulados: leads de anúncio recuperam title / body /
thumbnailUrl / sourceUrl, mensagens de texto comum ficam inalteradas, e mídia
citando outra mensagem recupera o stanzaId. Está rodando na nossa instância de
produção; ainda não observamos um lead de anúncio real passando por ele.

Dominant language
TypeScript
Stars
9.6k
Forks
7.3k
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from evolution-foundation/evolution-api

All issues in evolution-foundation/evolution-api

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.