From 719280d73712e37b73c2c4fe5cb3aa5a27793395 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Feb 2026 19:24:03 +0000 Subject: [PATCH] refactor(bluebubbles): share multipart helpers --- extensions/bluebubbles/src/attachments.ts | 26 +++++------------- extensions/bluebubbles/src/chat.ts | 25 +++++------------- extensions/bluebubbles/src/multipart.ts | 32 +++++++++++++++++++++++ 3 files changed, 44 insertions(+), 39 deletions(-) create mode 100644 extensions/bluebubbles/src/multipart.ts diff --git a/extensions/bluebubbles/src/attachments.ts b/extensions/bluebubbles/src/attachments.ts index 917079b3ae..31399ba70b 100644 --- a/extensions/bluebubbles/src/attachments.ts +++ b/extensions/bluebubbles/src/attachments.ts @@ -2,11 +2,11 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk"; import crypto from "node:crypto"; import path from "node:path"; import { resolveBlueBubblesAccount } from "./accounts.js"; +import { postMultipartFormData } from "./multipart.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js"; import { resolveChatGuidForTarget } from "./send.js"; import { - blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl, type BlueBubblesAttachment, type BlueBubblesSendTarget, @@ -219,26 +219,12 @@ export async function sendBlueBubblesAttachment(params: { // Close the multipart body parts.push(encoder.encode(`--${boundary}--\r\n`)); - // Combine all parts into a single buffer - const totalLength = parts.reduce((acc, part) => acc + part.length, 0); - const body = new Uint8Array(totalLength); - let offset = 0; - for (const part of parts) { - body.set(part, offset); - offset += part.length; - } - - const res = await blueBubblesFetchWithTimeout( + const res = await postMultipartFormData({ url, - { - method: "POST", - headers: { - "Content-Type": `multipart/form-data; boundary=${boundary}`, - }, - body, - }, - opts.timeoutMs ?? 60_000, // longer timeout for file uploads - ); + boundary, + parts, + timeoutMs: opts.timeoutMs ?? 60_000, // longer timeout for file uploads + }); if (!res.ok) { const errorText = await res.text(); diff --git a/extensions/bluebubbles/src/chat.ts b/extensions/bluebubbles/src/chat.ts index bfb37a4ddf..7e25c2cec8 100644 --- a/extensions/bluebubbles/src/chat.ts +++ b/extensions/bluebubbles/src/chat.ts @@ -2,6 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk"; import crypto from "node:crypto"; import path from "node:path"; import { resolveBlueBubblesAccount } from "./accounts.js"; +import { postMultipartFormData } from "./multipart.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js"; @@ -376,26 +377,12 @@ export async function setGroupIconBlueBubbles( // Close multipart body parts.push(encoder.encode(`--${boundary}--\r\n`)); - // Combine into single buffer - const totalLength = parts.reduce((acc, part) => acc + part.length, 0); - const body = new Uint8Array(totalLength); - let offset = 0; - for (const part of parts) { - body.set(part, offset); - offset += part.length; - } - - const res = await blueBubblesFetchWithTimeout( + const res = await postMultipartFormData({ url, - { - method: "POST", - headers: { - "Content-Type": `multipart/form-data; boundary=${boundary}`, - }, - body, - }, - opts.timeoutMs ?? 60_000, // longer timeout for file uploads - ); + boundary, + parts, + timeoutMs: opts.timeoutMs ?? 60_000, // longer timeout for file uploads + }); if (!res.ok) { const errorText = await res.text().catch(() => ""); diff --git a/extensions/bluebubbles/src/multipart.ts b/extensions/bluebubbles/src/multipart.ts new file mode 100644 index 0000000000..c3cf298338 --- /dev/null +++ b/extensions/bluebubbles/src/multipart.ts @@ -0,0 +1,32 @@ +import { blueBubblesFetchWithTimeout } from "./types.js"; + +export function concatUint8Arrays(parts: Uint8Array[]): Uint8Array { + const totalLength = parts.reduce((acc, part) => acc + part.length, 0); + const body = new Uint8Array(totalLength); + let offset = 0; + for (const part of parts) { + body.set(part, offset); + offset += part.length; + } + return body; +} + +export async function postMultipartFormData(params: { + url: string; + boundary: string; + parts: Uint8Array[]; + timeoutMs: number; +}): Promise { + const body = concatUint8Arrays(params.parts); + return await blueBubblesFetchWithTimeout( + params.url, + { + method: "POST", + headers: { + "Content-Type": `multipart/form-data; boundary=${params.boundary}`, + }, + body, + }, + params.timeoutMs, + ); +}