Skip to content

fix(bluebubbles): allow updated-message events with attachments through webhook filter#63983

Closed
coletebou wants to merge 1 commit intoopenclaw:mainfrom
coletebou:fix/bluebubbles-updated-message-attachments
Closed

fix(bluebubbles): allow updated-message events with attachments through webhook filter#63983
coletebou wants to merge 1 commit intoopenclaw:mainfrom
coletebou:fix/bluebubbles-updated-message-attachments

Conversation

@coletebou
Copy link
Copy Markdown
Contributor

Problem

BlueBubbles fires new-message first, then updated-message ~2-3s later once attachment data is fully available. The webhook handler unconditionally discards updated-message events unless they carry a reaction, silently dropping all inbound images, PDFs, and files that arrive via the update path.

Fix

Check for attachments in the updated-message payload before discarding. When attachments are present, let the event through for normal message processing.

Details

The attachment metadata lives in payload.data.attachments (or payload.attachments in older BB versions). The fix inspects both locations and only discards updated-message events that have neither a reaction nor attachments.

Related: #4848

…gh webhook filter

BlueBubbles fires new-message first, then updated-message ~2-3s later once
attachment data is fully available. The webhook handler unconditionally
discards updated-message events unless they carry a reaction, silently
dropping all inbound images, PDFs, and files that arrive via the update path.

Check for attachments in the updated-message payload before discarding.
When attachments are present, let the event through for normal processing.

Closes openclaw#4848
@openclaw-barnacle openclaw-barnacle Bot added channel: bluebubbles Channel integration: bluebubbles size: XS labels Apr 10, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3db31047e3

ℹ️ 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".

Comment on lines +272 to +273
!reaction &&
!updatedMessageHasAttachments
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Prevent duplicate turns from delayed attachment updates

Allowing updated-message events with attachments through this filter causes the same user message to be processed twice when BlueBubbles sends new-message first and the attachment-bearing update 2–3 seconds later (as noted in the comment). The default inbound debounce window is only 500ms in extensions/bluebubbles/src/monitor-debounce.ts, and processMessage has no inbound message-id dedupe path, so both events can trigger separate agent replies for a single message with media.

Useful? React with 👍 / 👎.

Comment on lines +261 to +263
const attachmentsFromRoot = Array.isArray(payloadRecord?.attachments)
? (payloadRecord.attachments as unknown[])
: [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require parseable message data before allowing root attachments

This branch explicitly treats payload.attachments as a signal to accept updated-message, but normalizeWebhookMessage only extracts message content from payload.message, payload.data, payload.payload, or payload.event (extractMessagePayload in extensions/bluebubbles/src/monitor-normalize.ts). For older payloads that expose attachments at the root without one of those message containers, this change turns a previously ignored event into a 400 "invalid payload" response, so attachments are still dropped and webhook retries can be triggered.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 10, 2026

Greptile Summary

This PR fixes a real gap: updated-message events carrying attachment data were unconditionally discarded, silently dropping inbound images, PDFs, and files that BlueBubbles delivers on the update path. The attachment-presence check added here is logically correct and covers both payload.data.attachments and the legacy payload.attachments location.

  • Duplicate delivery risk for text+attachment messages: processMessage has no messageId-based deduplication on the inbound path, and the debounce window (500 ms) is shorter than the reported BB updated-message delay (~2–3 s). If BlueBubbles fires updated-message for sends that have both text and an attachment, the same text body will be dispatched twice — once from new-message and once from updated-message. Whether this actually occurs depends on BB's exact sequencing behavior and warrants confirmation or a short-lived seen-set guard.

Confidence Score: 4/5

Safe to merge if BlueBubbles only fires updated-message for attachment-only sends; risky if it also fires for text+attachment sends due to missing messageId deduplication in processMessage.

One P1 concern: the 500 ms debounce window is shorter than the ~2–3 s updated-message delay, and processMessage has no messageId-based deduplication, so text+attachment sends could produce duplicate message delivery. The fix is a clear improvement for attachment-only messages, but confirmation of BlueBubbles sequencing behavior or a deduplication guard would make it safe to merge without reservation.

extensions/bluebubbles/src/monitor.ts — specifically the updated-message pass-through path and whether processMessage needs a short-lived seen-set guard by messageId.

Comments Outside Diff (1)

  1. extensions/bluebubbles/src/monitor.ts, line 277-283 (link)

    P2 Stale log message

    The verbose log still says "without reaction" even though the filter now also passes events that have attachments. When an updated-message with no reaction and no attachment is dropped, the log is accurate for that case, but readers may be confused about why an attachment-carrying event was passed through while the drop-path still says "without reaction". Consider updating it:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/bluebubbles/src/monitor.ts
    Line: 277-283
    
    Comment:
    **Stale log message**
    
    The verbose log still says "without reaction" even though the filter now also passes events that have attachments. When an `updated-message` with no reaction and no attachment is dropped, the log is accurate for that case, but readers may be confused about why an attachment-carrying event was passed through while the drop-path still says "without reaction". Consider updating it:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex Fix in Claude Code

Fix All in Codex Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor.ts
Line: 266-273

Comment:
**Possible duplicate delivery for text-plus-attachment messages**

When a sender sends a message that has both text and an attachment, BlueBubbles fires `new-message` (text only, attachment not yet available) and then `updated-message` ~2–3 s later (same text, full attachment). The fix lets the `updated-message` through, which is the right call for attachment-only sends. However, for text+attachment, `normalizeWebhookMessage` will extract the same non-empty text from both events, producing a non-empty `rawBody` each time. `processMessage` has no deduplication by `messageId` on the inbound path, so the same text body would be dispatched to the session twice — first without the attachment placeholder and then with it.

This is the crucial code path:

```
new-message   → rawBody = "some text"        → enqueued → delivered
updated-message (2–3 s later, outside 500 ms debounce window)
              → rawBody = "some text"        → enqueued → delivered again
```

If BlueBubbles guarantees `updated-message` only fires for attachment-only sends (no accompanying text), this is fine. If it fires for text+attachment, users will see a duplicate line.

Consider checking the `messageId` against a short-lived seen-set before enqueueing (deduplication window of ~5 s would cover the `updated-message` delay), or restricting the pass-through to cases where `text` is empty so only attachment-only events are affected.

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: 264-267

Comment:
**Misleading variable name and no-op alias**

`hasAttachments` is named like a boolean but is assigned an array, which makes `hasAttachments.length > 0` feel redundant on inspection. `payloadRecord` is also a pure alias of `payload` that adds no value. The full block can be simplified:

```suggestion
      const dataRecord = (payload as Record<string, unknown> & { data?: Record<string, unknown> }).data;
      const attachmentsFromData = Array.isArray(dataRecord?.attachments)
        ? (dataRecord.attachments as unknown[])
        : [];
      const attachmentsFromRoot = Array.isArray(payload.attachments)
        ? (payload.attachments as unknown[])
        : [];
      const hasAttachments = attachmentsFromData.length > 0 || attachmentsFromRoot.length > 0;
      const updatedMessageHasAttachments = eventType === "updated-message" && hasAttachments;
```

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: 277-283

Comment:
**Stale log message**

The verbose log still says "without reaction" even though the filter now also passes events that have attachments. When an `updated-message` with no reaction and no attachment is dropped, the log is accurate for that case, but readers may be confused about why an attachment-carrying event was passed through while the drop-path still says "without reaction". Consider updating it:

```suggestion
            `webhook ignored ${eventType || "event"} without reaction or attachments`,
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(bluebubbles): allow updated-message ..." | Re-trigger Greptile

Comment on lines +266 to +273
const updatedMessageHasAttachments =
eventType === "updated-message" && hasAttachments.length > 0;
if (
(eventType === "updated-message" ||
eventType === "message-reaction" ||
eventType === "reaction") &&
!reaction
!reaction &&
!updatedMessageHasAttachments
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Possible duplicate delivery for text-plus-attachment messages

When a sender sends a message that has both text and an attachment, BlueBubbles fires new-message (text only, attachment not yet available) and then updated-message ~2–3 s later (same text, full attachment). The fix lets the updated-message through, which is the right call for attachment-only sends. However, for text+attachment, normalizeWebhookMessage will extract the same non-empty text from both events, producing a non-empty rawBody each time. processMessage has no deduplication by messageId on the inbound path, so the same text body would be dispatched to the session twice — first without the attachment placeholder and then with it.

This is the crucial code path:

new-message   → rawBody = "some text"        → enqueued → delivered
updated-message (2–3 s later, outside 500 ms debounce window)
              → rawBody = "some text"        → enqueued → delivered again

If BlueBubbles guarantees updated-message only fires for attachment-only sends (no accompanying text), this is fine. If it fires for text+attachment, users will see a duplicate line.

Consider checking the messageId against a short-lived seen-set before enqueueing (deduplication window of ~5 s would cover the updated-message delay), or restricting the pass-through to cases where text is empty so only attachment-only events are affected.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor.ts
Line: 266-273

Comment:
**Possible duplicate delivery for text-plus-attachment messages**

When a sender sends a message that has both text and an attachment, BlueBubbles fires `new-message` (text only, attachment not yet available) and then `updated-message` ~2–3 s later (same text, full attachment). The fix lets the `updated-message` through, which is the right call for attachment-only sends. However, for text+attachment, `normalizeWebhookMessage` will extract the same non-empty text from both events, producing a non-empty `rawBody` each time. `processMessage` has no deduplication by `messageId` on the inbound path, so the same text body would be dispatched to the session twice — first without the attachment placeholder and then with it.

This is the crucial code path:

```
new-message   → rawBody = "some text"        → enqueued → delivered
updated-message (2–3 s later, outside 500 ms debounce window)
              → rawBody = "some text"        → enqueued → delivered again
```

If BlueBubbles guarantees `updated-message` only fires for attachment-only sends (no accompanying text), this is fine. If it fires for text+attachment, users will see a duplicate line.

Consider checking the `messageId` against a short-lived seen-set before enqueueing (deduplication window of ~5 s would cover the `updated-message` delay), or restricting the pass-through to cases where `text` is empty so only attachment-only events are affected.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex Fix in Claude Code

Comment on lines +264 to +267
const hasAttachments =
attachmentsFromData.length > 0 ? attachmentsFromData : attachmentsFromRoot;
const updatedMessageHasAttachments =
eventType === "updated-message" && hasAttachments.length > 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Misleading variable name and no-op alias

hasAttachments is named like a boolean but is assigned an array, which makes hasAttachments.length > 0 feel redundant on inspection. payloadRecord is also a pure alias of payload that adds no value. The full block can be simplified:

Suggested change
const hasAttachments =
attachmentsFromData.length > 0 ? attachmentsFromData : attachmentsFromRoot;
const updatedMessageHasAttachments =
eventType === "updated-message" && hasAttachments.length > 0;
const dataRecord = (payload as Record<string, unknown> & { data?: Record<string, unknown> }).data;
const attachmentsFromData = Array.isArray(dataRecord?.attachments)
? (dataRecord.attachments as unknown[])
: [];
const attachmentsFromRoot = Array.isArray(payload.attachments)
? (payload.attachments as unknown[])
: [];
const hasAttachments = attachmentsFromData.length > 0 || attachmentsFromRoot.length > 0;
const updatedMessageHasAttachments = eventType === "updated-message" && hasAttachments;
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor.ts
Line: 264-267

Comment:
**Misleading variable name and no-op alias**

`hasAttachments` is named like a boolean but is assigned an array, which makes `hasAttachments.length > 0` feel redundant on inspection. `payloadRecord` is also a pure alias of `payload` that adds no value. The full block can be simplified:

```suggestion
      const dataRecord = (payload as Record<string, unknown> & { data?: Record<string, unknown> }).data;
      const attachmentsFromData = Array.isArray(dataRecord?.attachments)
        ? (dataRecord.attachments as unknown[])
        : [];
      const attachmentsFromRoot = Array.isArray(payload.attachments)
        ? (payload.attachments as unknown[])
        : [];
      const hasAttachments = attachmentsFromData.length > 0 || attachmentsFromRoot.length > 0;
      const updatedMessageHasAttachments = eventType === "updated-message" && hasAttachments;
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

@coletebou
Copy link
Copy Markdown
Contributor Author

Closing in favor of a rewrite that addresses review feedback: simplified variable naming, data-only attachment check (not root payload), and text stripping to prevent duplicate delivery. New PR incoming.

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

Labels

channel: bluebubbles Channel integration: bluebubbles size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant