From 0d7d1c4208c2914d533d2d912a157c78c05a3218 Mon Sep 17 00:00:00 2001 From: Omar Shahine Date: Thu, 16 Apr 2026 14:46:10 +0000 Subject: [PATCH] fix(bluebubbles): move attachment retry before rawBody guard, fix stale log Move the attachment retry block (2s BB API refetch for empty attachments) before the !rawBody early-return guard. Previously, image-only messages with text='' and attachments=[] would be dropped by the !rawBody check before the retry could fire, making fix #4 dead code for its primary use-case. Now the retry runs first and recomputes the placeholder from resolved attachments so rawBody becomes non-empty when media is found. Also fix stale log message that still said 'without reaction' after the filter was expanded to pass through attachment updates. --- .../bluebubbles/src/monitor-processing.ts | 92 ++++++++++--------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/extensions/bluebubbles/src/monitor-processing.ts b/extensions/bluebubbles/src/monitor-processing.ts index 26c82834c2a..6af6d994f91 100644 --- a/extensions/bluebubbles/src/monitor-processing.ts +++ b/extensions/bluebubbles/src/monitor-processing.ts @@ -696,7 +696,51 @@ async function processMessageAfterDedupe( const text = message.text.trim(); let attachments = message.attachments ?? []; - let placeholder = buildMessagePlaceholder(message); + const baseUrl = normalizeSecretInputString(account.config.serverUrl); + const password = normalizeSecretInputString(account.config.password); + + // BlueBubbles may fire the webhook before attachment indexing is complete, + // so the initial `attachments` array can be empty for messages that actually + // have media. When the message text is empty (image-only) or this is an + // `updated-message` event, wait briefly and re-fetch from the BB API as a + // fallback for cases where BB doesn't send a follow-up webhook. (#65430, #67437) + // This must run before the !rawBody guard below, otherwise image-only messages + // with empty attachments are dropped before the retry can fire. + const retryMessageId = message.messageId?.trim(); + const shouldRetryAttachments = + attachments.length === 0 && + retryMessageId && + baseUrl && + password && + (text.length === 0 || message.eventType === "updated-message"); + if (shouldRetryAttachments) { + try { + await new Promise((resolve) => setTimeout(resolve, 2_000)); + const fetched = await fetchBlueBubblesMessageAttachments(retryMessageId, { + baseUrl, + password, + timeoutMs: 10_000, + allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config), + }); + if (fetched.length > 0) { + logVerbose( + core, + runtime, + `attachment retry found ${fetched.length} attachment(s) for msgId=${message.messageId}`, + ); + attachments = fetched; + } + } catch (err) { + logVerbose( + core, + runtime, + `attachment retry failed for msgId=${message.messageId}: ${String(err)}`, + ); + } + } + + // Recompute placeholder from resolved attachments (may have been updated by retry). + const placeholder = buildMessagePlaceholder({ ...message, attachments }); // Check if text is a tapback pattern (e.g., 'Loved "hello"') and transform to emoji format // For tapbacks, we'll append [[reply_to:N]] at the end; for regular messages, prepend it const tapbackContext = resolveTapbackContext(message); @@ -1074,9 +1118,6 @@ async function processMessageAfterDedupe( return; } - const baseUrl = normalizeSecretInputString(account.config.serverUrl); - const password = normalizeSecretInputString(account.config.password); - if (isGroup && !message.participants?.length && baseUrl && password) { try { const fetchedParticipants = await fetchBlueBubblesParticipantsForInboundMessage({ @@ -1120,53 +1161,14 @@ async function processMessageAfterDedupe( ? account.config.mediaMaxMb * 1024 * 1024 : 8 * 1024 * 1024; - // BlueBubbles may fire the webhook before attachment indexing is complete, - // so the initial `attachments` array can be empty for messages that actually - // have media. When the message text is empty (image-only) or this is an - // `updated-message` event, wait briefly and re-fetch from the BB API as a - // fallback for cases where BB doesn't send a follow-up webhook. (#65430, #67437) - let resolvedAttachments = attachments; - const retryMessageId = message.messageId?.trim(); - const shouldRetryAttachments = - resolvedAttachments.length === 0 && - retryMessageId && - baseUrl && - password && - (text.length === 0 || message.eventType === "updated-message"); - if (shouldRetryAttachments) { - try { - await new Promise((resolve) => setTimeout(resolve, 2_000)); - const fetched = await fetchBlueBubblesMessageAttachments(retryMessageId, { - baseUrl, - password, - timeoutMs: 10_000, - allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config), - }); - if (fetched.length > 0) { - logVerbose( - core, - runtime, - `attachment retry found ${fetched.length} attachment(s) for msgId=${message.messageId}`, - ); - resolvedAttachments = fetched; - } - } catch (err) { - logVerbose( - core, - runtime, - `attachment retry failed for msgId=${message.messageId}: ${String(err)}`, - ); - } - } - let mediaUrls: string[] = []; let mediaPaths: string[] = []; let mediaTypes: string[] = []; - if (resolvedAttachments.length > 0) { + if (attachments.length > 0) { if (!baseUrl || !password) { logVerbose(core, runtime, "attachment download skipped (missing serverUrl/password)"); } else { - for (const attachment of resolvedAttachments) { + for (const attachment of attachments) { if (!attachment.guid) { continue; }