mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-09 15:35:17 +00:00
refactor(msteams,bluebubbles): dedupe inbound media download helpers
This commit is contained in:
@@ -24,7 +24,11 @@ const fetchRemoteMediaMock = vi.fn(
|
||||
}
|
||||
const buffer = Buffer.from(await res.arrayBuffer());
|
||||
if (typeof params.maxBytes === "number" && buffer.byteLength > params.maxBytes) {
|
||||
throw new Error(`payload exceeds maxBytes ${params.maxBytes}`);
|
||||
const error = new Error(`payload exceeds maxBytes ${params.maxBytes}`) as Error & {
|
||||
code?: string;
|
||||
};
|
||||
error.code = "max_bytes";
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
buffer,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
||||
import { postMultipartFormData } from "./multipart.js";
|
||||
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
||||
import { resolveRequestUrl } from "./request-url.js";
|
||||
import { getBlueBubblesRuntime } from "./runtime.js";
|
||||
import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js";
|
||||
import { resolveChatGuidForTarget } from "./send.js";
|
||||
@@ -58,17 +59,16 @@ function resolveAccount(params: BlueBubblesAttachmentOpts) {
|
||||
return resolveBlueBubblesServerAccount(params);
|
||||
}
|
||||
|
||||
function resolveRequestUrl(input: RequestInfo | URL): string {
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
type MediaFetchErrorCode = "max_bytes" | "http_error" | "fetch_failed";
|
||||
|
||||
function readMediaFetchErrorCode(error: unknown): MediaFetchErrorCode | undefined {
|
||||
if (!error || typeof error !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
if (typeof input === "object" && input && "url" in input && typeof input.url === "string") {
|
||||
return input.url;
|
||||
}
|
||||
return String(input);
|
||||
const code = (error as { code?: unknown }).code;
|
||||
return code === "max_bytes" || code === "http_error" || code === "fetch_failed"
|
||||
? code
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export async function downloadBlueBubblesAttachment(
|
||||
@@ -103,10 +103,10 @@ export async function downloadBlueBubblesAttachment(
|
||||
contentType: fetched.contentType ?? attachment.mimeType ?? undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
if (/(?:maxBytes|content length|payload exceeds)/i.test(text)) {
|
||||
if (readMediaFetchErrorCode(error) === "max_bytes") {
|
||||
throw new Error(`BlueBubbles attachment too large (limit ${maxBytes} bytes)`);
|
||||
}
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`BlueBubbles attachment download failed: ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
12
extensions/bluebubbles/src/request-url.ts
Normal file
12
extensions/bluebubbles/src/request-url.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function resolveRequestUrl(input: RequestInfo | URL): string {
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
if (typeof input === "object" && input && "url" in input && typeof input.url === "string") {
|
||||
return input.url;
|
||||
}
|
||||
return String(input);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getMSTeamsRuntime } from "../runtime.js";
|
||||
import { downloadAndStoreMSTeamsRemoteMedia } from "./remote-media.js";
|
||||
import {
|
||||
extractInlineImageCandidates,
|
||||
inferPlaceholder,
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
isRecord,
|
||||
isUrlAllowed,
|
||||
normalizeContentType,
|
||||
resolveRequestUrl,
|
||||
resolveAuthAllowedHosts,
|
||||
resolveAllowedHosts,
|
||||
} from "./shared.js";
|
||||
@@ -149,19 +151,6 @@ async function fetchWithAuthFallback(params: {
|
||||
return firstAttempt;
|
||||
}
|
||||
|
||||
function resolveRequestUrl(input: RequestInfo | URL): string {
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
if (typeof input === "object" && input && "url" in input && typeof input.url === "string") {
|
||||
return input.url;
|
||||
}
|
||||
return String(input);
|
||||
}
|
||||
|
||||
function readRedirectUrl(baseUrl: string, res: Response): string | null {
|
||||
if (![301, 302, 303, 307, 308].includes(res.status)) {
|
||||
return null;
|
||||
@@ -258,8 +247,13 @@ export async function downloadMSTeamsAttachments(params: {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const fetched = await getMSTeamsRuntime().channel.media.fetchRemoteMedia({
|
||||
const media = await downloadAndStoreMSTeamsRemoteMedia({
|
||||
url: candidate.url,
|
||||
filePathHint: candidate.fileHint ?? candidate.url,
|
||||
maxBytes: params.maxBytes,
|
||||
contentTypeHint: candidate.contentTypeHint,
|
||||
placeholder: candidate.placeholder,
|
||||
preserveFilenames: params.preserveFilenames,
|
||||
fetchImpl: (input, init) =>
|
||||
fetchWithAuthFallback({
|
||||
url: resolveRequestUrl(input),
|
||||
@@ -269,27 +263,8 @@ export async function downloadMSTeamsAttachments(params: {
|
||||
allowHosts,
|
||||
authAllowHosts,
|
||||
}),
|
||||
filePathHint: candidate.fileHint ?? candidate.url,
|
||||
maxBytes: params.maxBytes,
|
||||
});
|
||||
const mime = await getMSTeamsRuntime().media.detectMime({
|
||||
buffer: fetched.buffer,
|
||||
headerMime: fetched.contentType,
|
||||
filePath: candidate.fileHint ?? candidate.url,
|
||||
});
|
||||
const originalFilename = params.preserveFilenames ? candidate.fileHint : undefined;
|
||||
const saved = await getMSTeamsRuntime().channel.media.saveMediaBuffer(
|
||||
fetched.buffer,
|
||||
mime ?? candidate.contentTypeHint,
|
||||
"inbound",
|
||||
params.maxBytes,
|
||||
originalFilename,
|
||||
);
|
||||
out.push({
|
||||
path: saved.path,
|
||||
contentType: saved.contentType,
|
||||
placeholder: candidate.placeholder,
|
||||
});
|
||||
out.push(media);
|
||||
} catch {
|
||||
// Ignore download failures and continue with next candidate.
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { getMSTeamsRuntime } from "../runtime.js";
|
||||
import { downloadMSTeamsAttachments } from "./download.js";
|
||||
import { downloadAndStoreMSTeamsRemoteMedia } from "./remote-media.js";
|
||||
import {
|
||||
GRAPH_ROOT,
|
||||
inferPlaceholder,
|
||||
isRecord,
|
||||
normalizeContentType,
|
||||
resolveRequestUrl,
|
||||
resolveAllowedHosts,
|
||||
} from "./shared.js";
|
||||
import type {
|
||||
@@ -14,19 +16,6 @@ import type {
|
||||
MSTeamsInboundMedia,
|
||||
} from "./types.js";
|
||||
|
||||
function resolveRequestUrl(input: RequestInfo | URL): string {
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
if (typeof input === "object" && input && "url" in input && typeof input.url === "string") {
|
||||
return input.url;
|
||||
}
|
||||
return String(input);
|
||||
}
|
||||
|
||||
type GraphHostedContent = {
|
||||
id?: string | null;
|
||||
contentType?: string | null;
|
||||
@@ -278,10 +267,12 @@ export async function downloadMSTeamsGraphMedia(params: {
|
||||
const encodedUrl = Buffer.from(shareUrl).toString("base64url");
|
||||
const sharesUrl = `${GRAPH_ROOT}/shares/u!${encodedUrl}/driveItem/content`;
|
||||
|
||||
const fetched = await getMSTeamsRuntime().channel.media.fetchRemoteMedia({
|
||||
const media = await downloadAndStoreMSTeamsRemoteMedia({
|
||||
url: sharesUrl,
|
||||
filePathHint: name,
|
||||
maxBytes: params.maxBytes,
|
||||
contentTypeHint: "application/octet-stream",
|
||||
preserveFilenames: params.preserveFilenames,
|
||||
fetchImpl: async (input, init) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
@@ -292,24 +283,7 @@ export async function downloadMSTeamsGraphMedia(params: {
|
||||
});
|
||||
},
|
||||
});
|
||||
const mime = await getMSTeamsRuntime().media.detectMime({
|
||||
buffer: fetched.buffer,
|
||||
headerMime: fetched.contentType,
|
||||
filePath: name,
|
||||
});
|
||||
const originalFilename = params.preserveFilenames ? name : undefined;
|
||||
const saved = await getMSTeamsRuntime().channel.media.saveMediaBuffer(
|
||||
fetched.buffer,
|
||||
mime ?? "application/octet-stream",
|
||||
"inbound",
|
||||
params.maxBytes,
|
||||
originalFilename,
|
||||
);
|
||||
sharePointMedia.push({
|
||||
path: saved.path,
|
||||
contentType: saved.contentType,
|
||||
placeholder: inferPlaceholder({ contentType: saved.contentType, fileName: name }),
|
||||
});
|
||||
sharePointMedia.push(media);
|
||||
downloadedReferenceUrls.add(shareUrl);
|
||||
} catch {
|
||||
// Ignore SharePoint download failures.
|
||||
|
||||
42
extensions/msteams/src/attachments/remote-media.ts
Normal file
42
extensions/msteams/src/attachments/remote-media.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getMSTeamsRuntime } from "../runtime.js";
|
||||
import { inferPlaceholder } from "./shared.js";
|
||||
import type { MSTeamsInboundMedia } from "./types.js";
|
||||
|
||||
type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
export async function downloadAndStoreMSTeamsRemoteMedia(params: {
|
||||
url: string;
|
||||
filePathHint: string;
|
||||
maxBytes: number;
|
||||
fetchImpl?: FetchLike;
|
||||
contentTypeHint?: string;
|
||||
placeholder?: string;
|
||||
preserveFilenames?: boolean;
|
||||
}): Promise<MSTeamsInboundMedia> {
|
||||
const fetched = await getMSTeamsRuntime().channel.media.fetchRemoteMedia({
|
||||
url: params.url,
|
||||
fetchImpl: params.fetchImpl,
|
||||
filePathHint: params.filePathHint,
|
||||
maxBytes: params.maxBytes,
|
||||
});
|
||||
const mime = await getMSTeamsRuntime().media.detectMime({
|
||||
buffer: fetched.buffer,
|
||||
headerMime: fetched.contentType ?? params.contentTypeHint,
|
||||
filePath: params.filePathHint,
|
||||
});
|
||||
const originalFilename = params.preserveFilenames ? params.filePathHint : undefined;
|
||||
const saved = await getMSTeamsRuntime().channel.media.saveMediaBuffer(
|
||||
fetched.buffer,
|
||||
mime ?? params.contentTypeHint,
|
||||
"inbound",
|
||||
params.maxBytes,
|
||||
originalFilename,
|
||||
);
|
||||
return {
|
||||
path: saved.path,
|
||||
contentType: saved.contentType,
|
||||
placeholder:
|
||||
params.placeholder ??
|
||||
inferPlaceholder({ contentType: saved.contentType, fileName: params.filePathHint }),
|
||||
};
|
||||
}
|
||||
@@ -63,6 +63,19 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function resolveRequestUrl(input: RequestInfo | URL): string {
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
if (typeof input === "object" && input && "url" in input && typeof input.url === "string") {
|
||||
return input.url;
|
||||
}
|
||||
return String(input);
|
||||
}
|
||||
|
||||
export function normalizeContentType(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user