Repository navigation
Conversation
Baileys answers a retry receipt with whatever getMessage returns. A message deleted for everyone keeps its content in the Message table, so when another device asks for a retry (for example after a group session is re-established on reconnection) the deleted text was relayed again and reappeared for the other members. getMessage now returns undefined on the non-full path when the message is marked deleted: on the row (status DELETED or key.deleted, set by deleteMessage with logical delete) or by a DELETED MessageUpdate for this instance and key id. The messages.update revoke branch now records its MessageUpdate as DELETED instead of the SERVER_ACK fallback, so revokes that arrive as events are covered too. The full path is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideStops Baileys retry receipts from resurrecting deleted message content by filtering deleted records in the retry-specific getMessage path and recording revoke events as DELETED, while leaving full-message retrieval unchanged. Sequence diagram for preventing deleted message retry resendssequenceDiagram
participant Device as GroupDevice
participant Baileys
participant Service as BaileysStartupService
participant Messages as MessageRepository
participant Updates as MessageUpdateRepository
Device->>Baileys: retry receipt
Baileys->>Service: getMessage(key, false)
Service->>Messages: find message by key
Messages-->>Service: message row
Service->>Updates: findFirst(instanceId, keyId, status=DELETED)
Updates-->>Service: deletion match or none
alt message is deleted
Service-->>Baileys: undefined
Baileys-->>Device: no resend
else message is available
Service-->>Baileys: message content
Baileys-->>Device: resend message
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="703-706" />
<code_context>
+ return true;
+ }
+
+ const deletion = await this.prismaRepository.messageUpdate.findFirst({
+ where: { instanceId: this.instanceId, keyId, status: 'DELETED' },
+ select: { id: true },
+ });
+
+ return !!deletion;
</code_context>
<issue_to_address>
**Another instance’s message is deleted**
When a shared-group deletion selects a different instance’s copy, `deleteMessage` marks the wrong instance’s row, leaving the requesting instance’s row live. Its deletion marker is absent or is later removed with the other row, so `isMessageDeleted` finds no deletion and a retry returns the content.
Scope the `deleteMessage` lookup to the current instance so it marks that instance’s message row.
Also at `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:1893-1925`.
</issue_to_address>
### Comment 2
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="703-706" />
<code_context>
+ return true;
+ }
+
+ const deletion = await this.prismaRepository.messageUpdate.findFirst({
+ where: { instanceId: this.instanceId, keyId, status: 'DELETED' },
+ select: { id: true },
+ });
+
+ return !!deletion;
</code_context>
<issue_to_address>
**Deletion checks scan update history**
When an instance has accumulated many `MessageUpdate` rows, the lookup filters by `instanceId`, `keyId`, and `status` without a matching index, so retry checks scan the instance’s update rows and add increasing database work.
Add an index that supports the deletion lookup’s filter fields.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and if the deletion check is wrong or misses a recorded deletion, a retry receipt can resend deleted message content to other devices, and reverting cannot undo that disclosure. If it incorrectly suppresses a live message, the delivery failure is bounded and can be repaired by retrying after a fix.
Blocking findings: src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:706, src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:706
|
Thanks @MatiasxVarela for picking this up in evolution-in-purple#38 and for the credit. Your version also marks the stored row as DELETED on revoke and scopes |
deleteMessage looked up the stored message by key id only. In a group shared by several instances the same key id exists once per instance, so the lookup could mark (or hard delete) another instance's copy and leave this instance's row live, which getMessage would then resend on a retry receipt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The messages.update revoke branch only recorded a DELETED MessageUpdate, and only when SAVE_DATA.MESSAGE_UPDATE is on. It now also marks this instance's Message row as DELETED, so getMessage refuses the retry resend without depending on that setting. A failure to write the mark is logged and the rest of the handler continues. The MessageUpdate check stays matched by instance + key id: tombstones written before deleteMessage was scoped can point at another instance's row, and the row mark can still be overwritten by a later EDITED or ack write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
📋 Description
A message deleted for everyone no longer comes back. Before this change, when a group member's device sent a retry receipt (common after a group session is re-established, for example when an instance reconnects or re-pairs), Evolution answered with the original content of the deleted message, and the text reappeared for the other members, sometimes days after it was deleted.
Why it happened. Baileys answers a retry receipt with whatever
getMessagereturns, and skips the resend only when it returns a falsy value (sendMessagesAgaininSocket/messages-recv). Deleting a message keeps its content in theMessagetable, andgetMessagenever looked at the deletion, so it returned the deleted payload and Baileys relayed it.What changed
getMessage(non-full path only) returnsundefinedwhen the message is marked deleted, so Baileys logsrecv retry request, but message not availableand sends nothing. Thefull = truepath used by edits, quoted replies, media download andupdateMessageis unchanged.status = 'DELETED'orkey.deleted = true(set bydeleteMessagewithLOGICAL_MESSAGE_DELETE), or when aDELETEDMessageUpdateexists for this instance and key id. The lookup is by instance and key id, not bymessageId: the row lookup indeleteMessageis not scoped to the instance, so in a group shared by several instances the deletion can be linked to another instance's copy (about 4% of deletions in the deployment we looked at).messages.updaterevoke branch (message === null) now stores itsMessageUpdateasDELETEDinstead of theSERVER_ACKfallback, so revokes that arrive as events (for example, deleted from the phone) are also honored. TheMESSAGES_DELETEwebhook already reported this status.How to reproduce (before this change)
DELETE /chat/deleteMessageForEveryone/{instance}). The phone shows "This message was deleted".Messagerows show up for the deleted key id with the full content.Checking it without a phone (PostgreSQL), messages of instance A that were deleted and later re-delivered:
Rows received after the deletion with
text_len > 0are deleted messages that were resent.To see who asked for the retry, run with
LOG_BAILEYS=debugand look forrecv retry request.Known limitation
Baileys keeps a recent-message cache (
enableRecentMessageCache, 512 messages, 5 minute TTL) and checks it before callinggetMessage. A retry that arrives within 5 minutes of the original send can still be answered from that cache, and Evolution does not control that path. The cases we observed were days old and are covered by this change.🔗 Related Issue
No open issue. Complements #2728, which stops
getMessagefrom returning{ conversation: '' }when the message is not found. The two changes do not overlap: #2728 covers a missing row, this PR covers a row that exists but was deleted.🧪 Type of Change
🧪 Testing
What was verified:
status = 'DELETED',key.deleted, deletion recorded only as an instance-scopedMessageUpdate, and aDELETEDupdate from another instance (must not count). All pass.tsc --noEmit,eslintandprettier --checkare clean.✅ Checklist
📝 Additional Notes
Trade-off: a device that asks for a retry of a deleted message receives nothing. That matches what "delete for everyone" promises; previously it received the deleted content.
🤖 Generated with Claude Code
Summary by Sourcery
Stop deleted WhatsApp messages from reappearing when retry receipts request their original content.
Bug Fixes: