Skip to content

fix: decrypt inbound message edits and clarify revoke webhooks#122

Open
cesar-carlos wants to merge 4 commits into
evolution-foundation:mainfrom
cesar-carlos:fix/decrypt-incoming-message-edit
Open

fix: decrypt inbound message edits and clarify revoke webhooks#122
cesar-carlos wants to merge 4 commits into
evolution-foundation:mainfrom
cesar-carlos:fix/decrypt-incoming-message-edit

Conversation

@cesar-carlos

@cesar-carlos cesar-carlos commented Jul 17, 2026

Copy link
Copy Markdown

Description

Incoming WhatsApp message edits from contacts were delivered as Message webhooks with only secretEncryptedMessage (no plaintext). Consumers (e.g. Chatwoot) could detect the edit and original message ID, but could not apply the new text.

This PR:

  1. Decrypts SecretEncryptedMessage with secretEncType = MESSAGE_EDIT via whatsmeow DecryptSecretEncryptedMessage before building the webhook payload.
  2. Runs decrypt before the LID→PN JID swap. Swapping first caused cipher: message authentication failed even when the message secret existed in whatsmeow_message_secrets (LID-keyed store).
  3. Unwraps the EditedMessage wrapper after decrypt (without calling UnwrapRaw, which would reset from RawMessage).
  4. Sets consistent flags: IsEdit, messageType: "edit", and protocolMessage.typeName: "MESSAGE_EDIT".
  5. On decrypt failure: keeps IsEdit / messageType: "edit" and adds decryptFailed: true (still no plaintext — documented).
  6. For revoke/delete for everyone: sets IsRevoke, messageType: "revoke", and protocolMessage.typeName: "REVOKE" so consumers do not rely only on opaque Info.Edit: "7" / numeric type: 0.
  7. Documents the contract in docs/wiki/recursos-avancados/events-system.md.

Outbound POST /message/edit was already working; this PR only fixes inbound edit/revoke webhook usability.

Related Issue

Closes #92

Related: #108 (same inbound edit plaintext gap; closed without a fix)

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement

Examples (after fix)

Inbound edit (contact edits on WhatsApp)

{
  "event": "Message",
  "data": {
    "Info": {
      "IsFromMe": false,
      "ID": "<edit-event-id>",
      "Edit": "1",
      "Type": "text"
    },
    "IsEdit": true,
    "messageType": "edit",
    "Message": {
      "protocolMessage": {
        "type": 14,
        "typeName": "MESSAGE_EDIT",
        "key": { "ID": "<ORIGINAL_MESSAGE_ID>" },
        "editedMessage": {
          "conversation": "Texto editado pelo cliente"
        }
      }
    }
  }
}

editedMessage.extendedTextMessage.text is also valid.

Consumer rules:

Need Field
Message to update Message.protocolMessage.key.ID (not Info.ID)
New text editedMessage.conversation or editedMessage.extendedTextMessage.text
Edit signal IsEdit / messageType: "edit" / Info.Edit: "1"

If decrypt fails: decryptFailed: true may be present with the encrypted envelope still in place.

Inbound revoke (delete for everyone)

{
  "event": "Message",
  "data": {
    "IsRevoke": true,
    "messageType": "revoke",
    "Message": {
      "protocolMessage": {
        "type": 0,
        "typeName": "REVOKE",
        "key": { "ID": "<ORIGINAL_MESSAGE_ID>" }
      }
    }
  }
}

Outbound edit via API (unchanged, still works)

POST /message/edit + webhook echo with IsFromMe: true and plaintext in editedMessage — no regression expected.

Testing

  • Manual testing completed
  • Functionality verified in development environment
  • No breaking changes introduced

Manual validation in production-like Docker:

  1. Contact sends text → webhook with body.
  2. Contact edits text → webhook contains protocolMessage.editedMessage plaintext (not only secretEncryptedMessage).
  3. Contact deletes for everyone → IsRevoke / typeName: "REVOKE".
  4. Confirmed root cause of failed decrypt after first attempt: MAC error when decrypt ran after LID→PN swap; fixed by decrypting first.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have tested my changes thoroughly
  • Any dependent changes have been merged and published

Additional Notes

  • Single concern: inbound edit/revoke webhook payload completeness for Missing information on message webhook when editing/deleting a message #92.
  • Files touched: pkg/whatsmeow/service/whatsmeow.go, docs/wiki/recursos-avancados/events-system.md.
  • No swagger / HTTP API changes.
  • Does not add new subscribe event types (MESSAGE_EDIT); edits/revokes remain under MESSAGE → event Message.

Made with Cursor

Summary by Sourcery

Improve inbound WhatsApp message edit and revoke webhook payloads for better consumer handling of edits and deletions.

Bug Fixes:

  • Decrypt inbound MESSAGE_EDIT secret encrypted payloads before JID normalization so edited message text is available in webhooks.
  • Preserve edit classification when edits arrive via legacy EditedMessage wrappers and when decryption fails.

Enhancements:

  • Add explicit webhook flags and protocolMessage.typeName for inbound edit and revoke events to make action type detection consistent.
  • Introduce a helper to set human-readable protocolMessage.typeName while retaining numeric type values in webhook payloads.

Documentation:

  • Document the structure and semantics of inbound edit and revoke webhook payloads, including decrypt behavior and limitations.

cesar-carlos and others added 4 commits July 17, 2026 18:24
Newer WhatsApp clients send edits as SecretEncryptedMessage; without
decrypting, Message webhooks arrived without the new text. Decrypt
MESSAGE_EDIT before marshal, set IsEdit, and document the contract.

Closes evolution-foundation#92

Co-authored-by: Cursor <cursoragent@cursor.com>
Expose IsRevoke, messageType, and protocolMessage.typeName so consumers
can detect deletes without relying on opaque Info.Edit or numeric type 0.

Part of evolution-foundation#92

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep IsEdit/messageType when decrypt fails, unwrap EditedMessage after
decrypt without resetting RawMessage, prefer clientPointer like polls,
and document key.ID plus the failure-path contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
DecryptSecretEncryptedMessage derives keys from wire Sender/Chat; swapping
LID to PN first caused MAC failures despite a stored message secret. Also
expose decryptFailed when plaintext cannot be recovered.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sourcery-ai

sourcery-ai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR fixes inbound WhatsApp message edit/revoke webhook usability by decrypting MESSAGE_EDIT secrets before JID normalization, unwrapping EditedMessage correctly, and setting explicit edit/revoke flags and protocolMessage.typeName, along with documenting the webhook contract.

Sequence diagram for inbound MESSAGE_EDIT decryption and webhook flags

sequenceDiagram
    participant WhatsAppServer
    participant MyClient
    participant WAClient
    participant WebhookConsumer

    WhatsAppServer ->> MyClient: myEventHandler(rawEvt)
    MyClient ->> MyClient: detect SecretEncryptedMessage_MESSAGE_EDIT
    MyClient ->> WAClient: DecryptSecretEncryptedMessage(ctx, evt)
    alt [decrypt ok]
        WAClient -->> MyClient: decrypted Message
        MyClient ->> MyClient: evt.Message = decrypted
        MyClient ->> MyClient: unwrap EditedMessage
        MyClient ->> MyClient: parsedMessageType = "edit"
        MyClient ->> MyClient: setProtocolMessageTypeName(dataMap, MESSAGE_EDIT)
        MyClient ->> WebhookConsumer: POST Message webhook
        Note right of WebhookConsumer: data.IsEdit = true\ndata.messageType = edit\nMessage.protocolMessage.typeName = MESSAGE_EDIT
    else [decrypt failed]
        WAClient -->> MyClient: error
        MyClient ->> MyClient: decryptFailed = true
        MyClient ->> MyClient: IsEdit = true, messageType = edit
        MyClient ->> WebhookConsumer: POST Message webhook
        Note right of WebhookConsumer: data.IsEdit = true\ndata.messageType = edit\ndata.decryptFailed = true\nno plaintext editedMessage
    end

    Note over MyClient: LID→PN JID swap happens after decrypt
Loading

File-Level Changes

Change Details Files
Decrypt inbound MESSAGE_EDIT envelopes before LID→PN JID swap and mark edit/decrypt state on the event.
  • Detect SecretEncryptedMessage with secretEncType MESSAGE_EDIT and mark the event as an edit.
  • Resolve the appropriate whatsmeow client instance and call DecryptSecretEncryptedMessage on the event before any JID swapping.
  • Handle missing client or decrypt errors by logging, preserving edit semantics, and tracking decryptFailed state.
  • On successful decrypt, replace evt.Message with the decrypted message and log success.
pkg/whatsmeow/service/whatsmeow.go
Normalize edited message structure and ensure IsEdit is consistently set for inbound edits.
  • Unwrap EditedMessage wrappers by replacing evt.Message with the inner Message without calling UnwrapRaw to avoid losing decrypted content.
  • Reassert evt.IsEdit when parsed messageType is "edit" or when a secret edit envelope was detected.
pkg/whatsmeow/service/whatsmeow.go
Expose explicit webhook flags and protocolMessage.typeName for edit and revoke events, including decrypt failure signalling.
  • Set IsEdit/messageType="edit" and protocolMessage.typeName="MESSAGE_EDIT" for edit payloads.
  • Set IsRevoke/messageType="revoke" and protocolMessage.typeName="REVOKE" for revoke payloads.
  • Ensure secret edit envelopes that haven’t been fully classified still surface IsEdit/messageType="edit".
  • Surface decryptFailed=true in webhook data when MESSAGE_EDIT decryption fails.
  • Introduce a helper to safely set protocolMessage.typeName inside the webhook Message map.
pkg/whatsmeow/service/whatsmeow.go
Document the inbound edit and revoke webhook payload contract and decrypt limitations.
  • Describe how inbound message edits remain under the Message event category and list the flags and fields carrying edit metadata and new text.
  • Explain decrypt ordering requirements relative to LID→PN normalization and behavior when decrypt fails (including decryptFailed flag).
  • Document revoke/delete-for-everyone payload semantics with explicit IsRevoke/messageType="revoke" and protocolMessage.typeName="REVOKE" and where to read the original message ID.
docs/wiki/recursos-avancados/events-system.md

Assessment against linked issues

Issue Objective Addressed Explanation
#92 Expose clear, explicit indicators in message webhooks for edit and delete-for-everyone (revoke) events, instead of relying only on opaque enums like Info.Edit or numeric protocolMessage.type.
#92 Include the new plaintext content of edited inbound messages in the webhook payload instead of only providing an encrypted secretEncryptedMessage envelope.

Possibly linked issues


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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • Consider reusing a local logger variable in the MESSAGE_EDIT decrypt block instead of calling loggerWrapper.GetLogger(mycli.userID) multiple times to reduce repetition and potential overhead.
  • The decrypt call uses context.Background(); if possible, pass through a request- or event-scoped context (with timeout/cancellation) to avoid potential hangs on slow or blocked decrypt operations.
  • The successful decrypt log is currently at info level and may become noisy under high edit volume; consider lowering it to debug or gating it behind a verbosity flag.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider reusing a local logger variable in the MESSAGE_EDIT decrypt block instead of calling `loggerWrapper.GetLogger(mycli.userID)` multiple times to reduce repetition and potential overhead.
- The decrypt call uses `context.Background()`; if possible, pass through a request- or event-scoped context (with timeout/cancellation) to avoid potential hangs on slow or blocked decrypt operations.
- The successful decrypt log is currently at info level and may become noisy under high edit volume; consider lowering it to debug or gating it behind a verbosity flag.

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.

cesar-carlos added a commit to cesar-carlos/evolution-go that referenced this pull request Jul 17, 2026
Inbound message edit decrypt + revoke webhook flags (PR evolution-foundation#122).
@cesar-carlos

Copy link
Copy Markdown
Author

Closing from the author side: these changes are already merged into our fork main for production use.

The commits remain available on the PR branch if maintainers still want to review/merge into evolution-foundation/evolution-go. Feel free to reopen if preferred.

@cesar-carlos
cesar-carlos deleted the fix/decrypt-incoming-message-edit branch July 17, 2026 20:08
@cesar-carlos
cesar-carlos restored the fix/decrypt-incoming-message-edit branch July 17, 2026 20:09
@cesar-carlos cesar-carlos reopened this Jul 17, 2026
@cesar-carlos

Copy link
Copy Markdown
Author

Reopened — keeping this PR available for upstream review/merge.

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.

Missing information on message webhook when editing/deleting a message

1 participant