Skip to content

fix: typing, recording-audio & reactions silently dropped (canonical JID)#73

Open
joaoporth wants to merge 4 commits into
evolution-foundation:mainfrom
joaoporth:fix/react-presence-silent-drop
Open

fix: typing, recording-audio & reactions silently dropped (canonical JID)#73
joaoporth wants to merge 4 commits into
evolution-foundation:mainfrom
joaoporth:fix/react-presence-silent-drop

Conversation

@joaoporth
Copy link
Copy Markdown

@joaoporth joaoporth commented Jun 4, 2026

Closes #63

What

Fix three WhatsApp features that returned 200 but never reached the recipient:

  • Typing (digitando...) and recording-audio (gravando áudio...) on POST /message/presence
  • Reactions on POST /message/react
  • Read receipts on POST /message/markread

…plus a delay option to sustain the typing/recording indicator.

Root cause

utils.CreateJID/ParseJID intentionally prefix phone numbers with + (e.g. +5541...@s.whatsapp.net) for the IsOnWhatsApp/display convention. Message sending tolerates this because whatsmeow normalizes the JID during usync/device resolution.

But several features send RAW nodes without usync, where the malformed +JID survives:

  • chatstate (typing/recording) — the node went out with to="+5541...@s.whatsapp.net", which WhatsApp does not route, so the indicator never appeared. Verified at the wire level.
  • reaction — the +JID is used both as the SendMessage target (can stall device resolution → usync timeout) and as the MessageKey.RemoteJID that references the reacted message's chat; a +-prefixed chat doesn't match the real chat, so the reaction never attaches to the message.
  • read receipts — same RAW-node issue.

Changes

  • utils.CanonicalJID — strips the leading + so RAW-node targets are canonical, digits-only WhatsApp JIDs. Covered by TestCanonicalJID.
  • Apply it in ChatPresence (typing/recording), React (recipient + group participant) and MarkRead (read receipts).
  • SendPresence(available) before the chatstate — WhatsApp only forwards chatstate while the sender is marked online; background presence handling can leave the client Unavailable, dropping the indicator.
  • delay (ms) on /message/presence — keeps the indicator alive for the requested duration (re-sending every 5s, capped at 60s) then sends paused, instead of a single ephemeral fire.

Testing

End-to-end on a live instance, confirmed on the recipient device:

  • digitando... and gravando áudio... appear (and persist for delay).
  • ✅ Reactions attach to the correct message, including multi-codepoint emoji (❤️), with no usync timeout.
  • go build, go vet, go test ./pkg/utils/ all green.

Examples

POST /message/presence
{ "number": "5541...", "state": "composing", "isAudio": false, "delay": 8000 }

POST /message/react
{ "number": "5541...", "id": "3EB0...", "reaction": "👍", "fromMe": true }

isAudio: truegravando áudio.... delay omitted/0 → legacy single-fire (unchanged).

🤖 Generated with Claude Code

joaoporth added 2 commits June 4, 2026 15:18
…SendChatPresence

- React: remove `ID: msgId` from SendRequestExtra so whatsmeow generates
  a fresh unique message ID for the reaction envelope. Passing the original
  message ID as the send ID caused WhatsApp to silently deduplicate and
  drop the reaction. The msgId still lives correctly inside MessageKey
  (the reference to the message being reacted to).

- ChatPresence: call client.SubscribePresence before SendChatPresence.
  WhatsApp requires an active presence subscription on the recipient JID
  before it forwards chatstate (typing/recording) events. Without it the
  call succeeds locally but the indicator never appears on the recipient side.
  SubscribePresence errors are logged as warnings and are non-fatal.

Fixes evolution-foundation#63 (typing indicator) and the silent reaction drop issue.
…uestExtra

Passing ID: msgId in SendRequestExtra caused WhatsApp to use the
original message ID as the reaction envelope ID. Since that ID is
already known to the server, it silently deduplicates and drops the
reaction. The msgId belongs only inside MessageKey (reference to the
message being reacted to). Let whatsmeow generate a fresh ID for the
reaction envelope.

Also update messageInfo.ID to use response.ID (the real generated ID)
instead of the original msgId.

ChatPresence is reverted to its original correct form. The
SubscribePresence call added previously was wrong: it is for receiving
presence updates from a contact, not for sending chatstate (typing)
events. SendChatPresence sends a chatstate node directly and does not
require SubscribePresence.
Copilot AI review requested due to automatic review settings June 4, 2026 19:54
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Jun 4, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Fixes WhatsApp reaction handling so that reactions are sent with a fresh envelope ID while still targeting the original message, preventing WhatsApp from silently deduplicating and dropping reactions, and ensures the stored message record uses the actual reaction envelope ID returned by the server.

Sequence diagram for updated WhatsApp reaction sending in React

sequenceDiagram
    participant Caller as Caller
    participant MessageService as messageService
    participant WhatsmeowClient as whatsmeowClient
    participant WhatsAppServer as WhatsApp
    participant DB as MessageStore

    Caller->>MessageService: React(data, instance)
    MessageService->>MessageService: Build MessageKey using msgId
    MessageService->>MessageService: Build waE2E.Message with ReactionMessage
    MessageService->>WhatsmeowClient: SendMessage(ctx, recipient, msg)
    WhatsmeowClient->>WhatsAppServer: Send reaction with new envelope ID
    WhatsAppServer-->>WhatsmeowClient: SendMessage response(ID, ServerID)
    WhatsmeowClient-->>MessageService: response
    MessageService->>DB: Store message with ID = response.ID
    MessageService-->>Caller: ReactResponse
Loading

File-Level Changes

Change Details Files
Correct reaction message envelope ID handling to avoid silent drops and persist the real server-assigned ID.
  • Clarified that msgId refers to the target message being reacted to when creating the MessageKey, not the reaction envelope itself.
  • Removed explicit ID: msgId from SendRequestExtra so whatsmeow can generate a unique reaction envelope ID, preventing deduplication by WhatsApp.
  • Updated local persistence to store response.ID (the reaction envelope ID) instead of reusing msgId for the reaction record.
pkg/message/service/message_service.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates reaction sending to avoid ID reuse so WhatsApp doesn’t deduplicate/drop reactions.

Changes:

  • Clarified that the MessageKey should reference the original message ID, not the reaction envelope ID.
  • Stopped passing SendRequestExtra{ID: msgId} so whatsmeow generates a fresh envelope ID.
  • Persisted the returned envelope ID (response.ID) instead of the original message ID (msgId).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

IsGroup: isGroup,
},
ID: msgId,
ID: response.ID,
// GroupingKey: proto.String(reaction),
Key: messageKey,
Text: proto.String(reaction),
SenderTimestampMS: proto.Int64(time.Now().UnixMilli()),
},
ID: msgId,
ID: response.ID,
Timestamp: time.Now(),
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@joaoporth
Copy link
Copy Markdown
Author

Fechando este PR. Após testes mais aprofundados, as correções não se sustentaram: a mudança de presence (SendPresence available + chatstate) envia os nós corretamente no protocolo, mas o indicador de "digitando..." não aparece de fato para o destinatário em nossos testes, e a correção de React não pôde ser validada (usync timeout impediu o teste). Vou revisar e reabrir/abrir um novo PR quando estiver realmente validado. Obrigado!

@joaoporth joaoporth closed this Jun 4, 2026
The /message/presence chatstate (typing & recording-audio) was silently
dropped: ParseJID/CreateJID prefixes phone numbers with '+'
(e.g. +5541...@s.whatsapp.net). Message sending tolerates this because
whatsmeow normalizes the JID during usync/device resolution, but RAW
nodes (chatstate, read receipts) are sent without usync, so the malformed
'+JID' reaches the server and the node is never routed to the recipient.

- Add utils.CanonicalJID to strip the leading '+' for RAW-node targets,
  with unit test (TestCanonicalJID).
- Apply it in ChatPresence (typing/recording) and MarkRead (read receipts).
- Send SendPresence(available) before the chatstate so the server forwards
  the indicator (it only relays chatstate while the sender is online).
- Add optional 'delay' (ms) to /message/presence: keep the indicator alive
  for the duration (re-sending every 5s, capped at 60s) then send 'paused',
  instead of a single ephemeral fire.

Tested end-to-end: 'digitando...' and 'gravando audio...' both confirmed
appearing on the recipient device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@joaoporth joaoporth reopened this Jun 5, 2026
@joaoporth joaoporth changed the title Fix/react presence silent drop fix(presence): deliver typing & recording-audio indicators (canonical JID) Jun 5, 2026
@joaoporth
Copy link
Copy Markdown
Author

Reabrindo: a causa-raiz foi identificada e corrigida. O chatstate (typing/recording) era enviado para um JID com prefixo + (malformado para nós RAW que não passam por usync), então o servidor não roteava. Com utils.CanonicalJID (strip do +) + SendPresence(available) antes do chatstate, os indicadores digitando... e gravando áudio... foram confirmados aparecendo no aparelho do destinatário em teste end-to-end. Também adicionado delay opcional para sustentar o indicador. Detalhes no corpo atualizado do PR.

The reaction (/message/react) was failing for the same root cause as the
presence indicators: ParseJID/CreateJID prefix the number with '+'. In
React that malformed JID is used in two places:

- as the SendMessage target (usync/device resolution), which could stall
  the send (usync timeout); and
- as the MessageKey RemoteJID that references the reacted message's chat —
  a '+'-prefixed chat JID does not match the real chat, so the reaction
  never attaches to the original message.

Apply utils.CanonicalJID to the recipient (covers both the SendMessage
target and the MessageKey RemoteJID) and to the optional group participant.

Tested end-to-end: reactions (including multi-codepoint emoji like ❤️)
confirmed attaching to the correct message on the recipient device, with
no usync timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@joaoporth joaoporth changed the title fix(presence): deliver typing & recording-audio indicators (canonical JID) fix: typing, recording-audio & reactions silently dropped (canonical JID) Jun 5, 2026
@joaoporth
Copy link
Copy Markdown
Author

Atualização: além dos indicadores de presença (digitando/gravando áudio), este PR agora também corrige as reações (/message/react) — mesma causa-raiz do + no JID. No React o JID malformado era usado tanto no SendMessage (resolução de devices/usync) quanto no MessageKey.RemoteJID que referencia o chat da mensagem reagida, então a reação não casava com a mensagem. Corrigido com utils.CanonicalJID. Testado end-to-end: reações (inclusive emoji multi-codepoint como ❤️) confirmadas grudando na mensagem certa, sem usync timeout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/message/presence retorna 200 mas typing nativo não aparece

2 participants