export async function downloadBlueBubblesAttachment(
attachment: BlueBubblesAttachment,
opts: BlueBubblesAttachmentOpts & { maxBytes?: number } = {},
): Promise<{ buffer: Uint8Array; contentType?: string }> { const client = clientFromOpts(opts); // client.downloadAttachment threads this.ssrfPolicy to BOTH fetchRemoteMedia // and the fetchImpl callback — closing the gap in #34749 where the legacy // helper silently omitted the policy on the callback path. return await client.downloadAttachment({
attachment,
maxBytes: opts.maxBytes,
timeoutMs: opts.timeoutMs,
});
}
export type SendBlueBubblesAttachmentResult = {
messageId: string;
};
/** *SendanattachmentviaBlueBubblesAPI. *Supportssendingmediafiles(images,videos,audio,documents)toachat. *WhenasVoiceistrue,expectsMP3/CAFaudioandmarksitasaniMessagevoicememo.
*/
export async function sendBlueBubblesAttachment(params: {
to: string;
buffer: Uint8Array;
filename: string;
contentType?: string;
caption?: string;
replyToMessageGuid?: string;
replyToPartIndex?: number;
asVoice?: boolean;
opts?: BlueBubblesAttachmentOpts;
}): Promise<SendBlueBubblesAttachmentResult> { const { to, caption, replyToMessageGuid, replyToPartIndex, asVoice, opts = {} } = params;
let { buffer, filename, contentType } = params; const wantsVoice = asVoice === true; const fallbackName = wantsVoice ? "Audio Message" : "attachment";
filename = sanitizeFilename(filename, fallbackName);
contentType = normalizeOptionalString(contentType); // Resolve account tuple for helpers that still need baseUrl/password // (createChatForHandle, resolveChatGuidForTarget, fetchBlueBubblesServerInfo). // These migrate to the client in subsequent passes. For this callsite, the // client owns the actual attachment POST; the resolved tuple stays alongside // so chat-guid resolution and Private API probe continue to work. const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(opts); const client = createBlueBubblesClient(opts);
let privateApiStatus = getCachedBlueBubblesPrivateApiStatus(accountId);
// Lazy refresh: when the cache has expired and Private API features are needed, // fetch server info before making the decision. This prevents silent degradation // of reply threading after the 10-minute cache TTL expires. (#43764) const wantsReplyThread = Boolean(replyToMessageGuid?.trim()); if (privateApiStatus === null && wantsReplyThread) { try {
await fetchBlueBubblesServerInfo({
baseUrl,
password,
accountId,
timeoutMs: opts.timeoutMs ?? 5000,
allowPrivateNetwork,
});
privateApiStatus = getCachedBlueBubblesPrivateApiStatus(accountId);
} catch { // Refresh failed — proceed with null status (existing graceful degradation)
}
}
// Validate voice memo format when requested (BlueBubbles converts MP3 -> CAF when isAudioMessage). const isAudioMessage = wantsVoice; if (isAudioMessage) { const voiceInfo = resolveVoiceInfo(filename, contentType); if (!voiceInfo.isAudio) { thrownew Error("BlueBubbles voice messages require audio media (mp3 or caf).");
} if (voiceInfo.isMp3) {
filename = ensureExtension(filename, ".mp3", fallbackName);
contentType = contentType ?? "audio/mpeg";
} elseif (voiceInfo.isCaf) {
filename = ensureExtension(filename, ".caf", fallbackName);
contentType = contentType ?? "audio/x-caf";
} else { thrownew Error( "BlueBubbles voice messages require mp3 or caf audio (convert before sending).",
);
}
}
const target = resolveBlueBubblesSendTarget(to);
let chatGuid = await resolveChatGuidForTarget({
baseUrl,
password,
timeoutMs: opts.timeoutMs,
target,
allowPrivateNetwork,
}); if (!chatGuid) { // For handle targets (phone numbers/emails), auto-create a new DM chat if (target.kind === "handle") { const created = await createChatForHandle({
baseUrl,
password,
address: target.address,
timeoutMs: opts.timeoutMs,
allowPrivateNetwork,
});
chatGuid = created.chatGuid; // If we still don't have a chatGuid, try resolving again (chat was created server-side) if (!chatGuid) {
chatGuid = await resolveChatGuidForTarget({
baseUrl,
password,
timeoutMs: opts.timeoutMs,
target,
allowPrivateNetwork,
});
}
} if (!chatGuid) { thrownew Error( "BlueBubbles attachment send failed: chatGuid not found for target. Use a chat_guid target or ensure the chat exists.",
);
}
}
// Build FormData with the attachment const boundary = `----BlueBubblesFormBoundary${crypto.randomUUID().replace(/-/g, "")}`; const parts: Uint8Array[] = []; const encoder = new TextEncoder();
// Helper to add a form field const addField = (name: string, value: string) => {
parts.push(encoder.encode(`--${boundary}\r\n`));
parts.push(encoder.encode(`Content-Disposition: form-data; name="${name}"\r\n\r\n`));
parts.push(encoder.encode(`${value}\r\n`));
};
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.