fix(bluebubbles): accept updated-message events carrying attachments#66105
fix(bluebubbles): accept updated-message events carrying attachments#66105coletebou wants to merge 1 commit intoopenclaw:mainfrom
Conversation
BlueBubbles fires new-message first (text only), then updated-message ~2-3s later once attachment data is ready. The webhook filter unconditionally discards updated-message events that are not reactions, silently dropping all inbound images, PDFs, and files. Let updated-message events through when data.attachments is non-empty. Strip the text body from these events to prevent duplicate delivery, since the text was already dispatched via new-message and the 500ms debounce window is too short to coalesce the ~2-3s delayed update. Only checks data.attachments (not root payload.attachments) because normalizeWebhookMessage requires a message container; root-only attachment payloads would pass the filter but fail normalization.
Greptile SummaryThis PR fixes a silent attachment-drop bug in the BlueBubbles webhook handler: Confidence Score: 5/5Safe to merge; all findings are P2 style/test suggestions that do not affect correctness. No P0 or P1 issues found. The logic is correct — isAttachmentUpdate is scoped to updated-message events, the attachment check targets the right nested path, and the text-strip is intentional and documented. Two P2 items remain: a redundant asRecord wrapper and missing test coverage for the new code paths. extensions/bluebubbles/src/monitor.ts — the two P2 comments above are both in this file. Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor.ts
Line: 258
Comment:
**Redundant outer `asRecord` call**
`payload` is already typed as `Record<string, unknown>` (from `const payload = asRecord(parsed.value) ?? {}` at line 225), so wrapping it in `asRecord(...)` again is a no-op. The `as Record<string, unknown> | undefined` cast is also unnecessary since `?.` handles `null` from `asNullableRecord` at runtime. Simplifying this makes the intent clearer:
```suggestion
const dataAttachments = asRecord(payload.data)?.attachments;
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor.ts
Line: 251-285
Comment:
**No tests for the new attachment-update code paths**
The test helpers (`createNewMessagePayloadForTest`, `dispatchWebhookPayloadForTest`) are already in place in `monitor.webhook.test-helpers.ts` and could be extended with an `updated-message` variant to cover: (1) attachment-only updates reaching the message processor, (2) text being stripped to prevent duplicates, and (3) attachment-less `updated-message` events still being filtered out. These three scenarios are exactly the test-plan bullets in the PR description and are currently untested.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(bluebubbles): accept updated-message..." | Re-trigger Greptile |
| // -- normalizeWebhookMessage requires a message container | ||
| // (payload.data/message/payload/event); root-only attachment payloads would | ||
| // pass the filter but fail normalization. | ||
| const dataAttachments = (asRecord(asRecord(payload)?.data) as Record<string, unknown> | undefined)?.attachments; |
There was a problem hiding this comment.
payload is already typed as Record<string, unknown> (from const payload = asRecord(parsed.value) ?? {} at line 225), so wrapping it in asRecord(...) again is a no-op. The as Record<string, unknown> | undefined cast is also unnecessary since ?. handles null from asNullableRecord at runtime. Simplifying this makes the intent clearer:
| const dataAttachments = (asRecord(asRecord(payload)?.data) as Record<string, unknown> | undefined)?.attachments; | |
| const dataAttachments = asRecord(payload.data)?.attachments; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor.ts
Line: 258
Comment:
**Redundant outer `asRecord` call**
`payload` is already typed as `Record<string, unknown>` (from `const payload = asRecord(parsed.value) ?? {}` at line 225), so wrapping it in `asRecord(...)` again is a no-op. The `as Record<string, unknown> | undefined` cast is also unnecessary since `?.` handles `null` from `asNullableRecord` at runtime. Simplifying this makes the intent clearer:
```suggestion
const dataAttachments = asRecord(payload.data)?.attachments;
```
How can I resolve this? If you propose a fix, please make it concise.| @@ -265,7 +276,13 @@ export async function handleBlueBubblesWebhookRequest( | |||
| } | |||
| return true; | |||
| } | |||
| const message = reaction ? null : normalizeWebhookMessage(payload); | |||
| let message = reaction ? null : normalizeWebhookMessage(payload); | |||
| // Strip text from attachment updates to prevent duplicate delivery. | |||
| // The text was already dispatched via new-message; the debounce window (500ms) | |||
| // is shorter than the updated-message delay (~2-3s) so they won't coalesce. | |||
| if (message && isAttachmentUpdate) { | |||
| message = { ...message, text: "" }; | |||
| } | |||
There was a problem hiding this comment.
No tests for the new attachment-update code paths
The test helpers (createNewMessagePayloadForTest, dispatchWebhookPayloadForTest) are already in place in monitor.webhook.test-helpers.ts and could be extended with an updated-message variant to cover: (1) attachment-only updates reaching the message processor, (2) text being stripped to prevent duplicates, and (3) attachment-less updated-message events still being filtered out. These three scenarios are exactly the test-plan bullets in the PR description and are currently untested.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor.ts
Line: 251-285
Comment:
**No tests for the new attachment-update code paths**
The test helpers (`createNewMessagePayloadForTest`, `dispatchWebhookPayloadForTest`) are already in place in `monitor.webhook.test-helpers.ts` and could be extended with an `updated-message` variant to cover: (1) attachment-only updates reaching the message processor, (2) text being stripped to prevent duplicates, and (3) attachment-less `updated-message` events still being filtered out. These three scenarios are exactly the test-plan bullets in the PR description and are currently untested.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a544b94cc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (message && isAttachmentUpdate) { | ||
| message = { ...message, text: "" }; | ||
| } |
There was a problem hiding this comment.
Keep mention-bearing text on attachment updates
Clearing message.text for every attachment-bearing updated-message causes group mention gating to drop those updates when requireMention is enabled. processMessage evaluates mentions from message.text (not the placeholder), so after this rewrite the attachment follow-up is treated as unmentioned and returns early, which still silently drops media in mention-gated groups even when the original text message mentioned the agent.
Useful? React with 👍 / 👎.
| const dataAttachments = (asRecord(asRecord(payload)?.data) as Record<string, unknown> | undefined)?.attachments; | ||
| const isAttachmentUpdate = | ||
| eventType === "updated-message" && Array.isArray(dataAttachments) && dataAttachments.length > 0; |
There was a problem hiding this comment.
Detect attachment updates across all normalized payload shapes
The attachment gate only inspects payload.data.attachments, but normalizeWebhookMessage accepts message containers in multiple forms (payload.message, payload.data.message, and stringified variants via extractMessagePayload). For valid updated-message payloads where attachments live under data.message.attachments (or parsed from string data), isAttachmentUpdate stays false and this event is still filtered out as a non-reaction.
Useful? React with 👍 / 👎.
|
Closing — review caught that stripping text breaks mention gating in group chats. Resubmitting with a fix that preserves text for mention evaluation. |
Summary
BlueBubbles fires
new-messagefirst (text only), thenupdated-message~2-3s later once attachment data is ready. The webhook filter unconditionally discardsupdated-messageevents that are not reactions, silently dropping all inbound images, PDFs, and files.This lets
updated-messageevents through whendata.attachmentsis non-empty, and strips the text body to prevent duplicate delivery (the text was already dispatched vianew-messageand the 500ms debounce window is too short to coalesce the ~2-3s delayed update).Supersedes #63983 which had several issues flagged in review:
payload.attachmentswhich could pass the filter but failnormalizeWebhookMessageChanges
extensions/bluebubbles/src/monitor.ts:updated-messageviadata.attachments(not root payload)new-message)Test plan
updated-messageevents without attachments are still filtered out