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.
This commit is contained in:
Omar Shahine
2026-04-16 14:46:10 +00:00
parent b02cc1e8f2
commit 0d7d1c4208

View File

@@ -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<void>((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<void>((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;
}