Step 3 + Review
This commit is contained in:
committed by
Peter Steinberger
parent
e9d691d472
commit
7870ce8177
@@ -1,3 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import type { ChannelAccountSnapshot, ChannelPlugin, ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
@@ -25,6 +28,7 @@ import { normalizeBlueBubblesHandle } from "./targets.js";
|
||||
import { bluebubblesMessageActions } from "./actions.js";
|
||||
import { monitorBlueBubblesProvider, resolveWebhookPathFromConfig } from "./monitor.js";
|
||||
import { blueBubblesOnboardingAdapter } from "./onboarding.js";
|
||||
import { getBlueBubblesRuntime } from "./runtime.js";
|
||||
|
||||
const meta = {
|
||||
id: "bluebubbles",
|
||||
@@ -36,6 +40,37 @@ const meta = {
|
||||
order: 75,
|
||||
};
|
||||
|
||||
const HTTP_URL_RE = /^https?:\/\//i;
|
||||
|
||||
function resolveLocalMediaPath(source: string): string {
|
||||
if (!source.startsWith("file://")) return source;
|
||||
try {
|
||||
return fileURLToPath(source);
|
||||
} catch {
|
||||
throw new Error(`Invalid file:// URL: ${source}`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFilenameFromSource(source?: string): string | undefined {
|
||||
if (!source) return undefined;
|
||||
if (source.startsWith("file://")) {
|
||||
try {
|
||||
return path.basename(fileURLToPath(source)) || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (HTTP_URL_RE.test(source)) {
|
||||
try {
|
||||
return path.basename(new URL(source).pathname) || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
const base = path.basename(source);
|
||||
return base || undefined;
|
||||
}
|
||||
|
||||
export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
id: "bluebubbles",
|
||||
meta,
|
||||
@@ -216,27 +251,69 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
});
|
||||
return { channel: "bluebubbles", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, mediaPath, mediaBuffer, contentType, filename, caption, accountId }) => {
|
||||
// Prefer buffer if provided, otherwise read from path
|
||||
sendMedia: async (ctx) => {
|
||||
const { cfg, to, text, mediaUrl, accountId } = ctx;
|
||||
const { mediaPath, mediaBuffer, contentType, filename, caption } = ctx as {
|
||||
mediaPath?: string;
|
||||
mediaBuffer?: Uint8Array;
|
||||
contentType?: string;
|
||||
filename?: string;
|
||||
caption?: string;
|
||||
};
|
||||
const core = getBlueBubblesRuntime();
|
||||
const resolvedCaption = caption ?? text;
|
||||
|
||||
let buffer: Uint8Array;
|
||||
let resolvedContentType = contentType ?? undefined;
|
||||
let resolvedFilename = filename ?? undefined;
|
||||
|
||||
if (mediaBuffer) {
|
||||
buffer = mediaBuffer;
|
||||
} else if (mediaPath) {
|
||||
const fs = await import("node:fs/promises");
|
||||
buffer = new Uint8Array(await fs.readFile(mediaPath));
|
||||
if (!resolvedContentType) {
|
||||
const hint = mediaPath ?? mediaUrl;
|
||||
const detected = await core.media.detectMime({
|
||||
buffer: Buffer.isBuffer(mediaBuffer) ? mediaBuffer : Buffer.from(mediaBuffer),
|
||||
filePath: hint,
|
||||
});
|
||||
resolvedContentType = detected ?? undefined;
|
||||
}
|
||||
if (!resolvedFilename) {
|
||||
resolvedFilename = resolveFilenameFromSource(mediaPath ?? mediaUrl);
|
||||
}
|
||||
} else {
|
||||
throw new Error("BlueBubbles media delivery requires mediaPath or mediaBuffer.");
|
||||
const source = mediaPath ?? mediaUrl;
|
||||
if (!source) {
|
||||
throw new Error("BlueBubbles media delivery requires mediaUrl, mediaPath, or mediaBuffer.");
|
||||
}
|
||||
if (HTTP_URL_RE.test(source)) {
|
||||
const fetched = await core.channel.media.fetchRemoteMedia({ url: source });
|
||||
buffer = fetched.buffer;
|
||||
resolvedContentType = resolvedContentType ?? fetched.contentType ?? undefined;
|
||||
resolvedFilename = resolvedFilename ?? fetched.fileName;
|
||||
} else {
|
||||
const localPath = resolveLocalMediaPath(source);
|
||||
const fs = await import("node:fs/promises");
|
||||
const data = await fs.readFile(localPath);
|
||||
buffer = new Uint8Array(data);
|
||||
if (!resolvedContentType) {
|
||||
const detected = await core.media.detectMime({
|
||||
buffer: data,
|
||||
filePath: localPath,
|
||||
});
|
||||
resolvedContentType = detected ?? undefined;
|
||||
}
|
||||
if (!resolvedFilename) {
|
||||
resolvedFilename = resolveFilenameFromSource(localPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve filename from path if not provided
|
||||
const resolvedFilename = filename ?? (mediaPath ? mediaPath.split("/").pop() ?? "attachment" : "attachment");
|
||||
|
||||
const result = await sendBlueBubblesAttachment({
|
||||
to,
|
||||
buffer,
|
||||
filename: resolvedFilename,
|
||||
contentType: contentType ?? undefined,
|
||||
caption: caption ?? undefined,
|
||||
filename: resolvedFilename ?? "attachment",
|
||||
contentType: resolvedContentType ?? undefined,
|
||||
caption: resolvedCaption ?? undefined,
|
||||
opts: {
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
accountId: accountId ?? undefined,
|
||||
|
||||
1357
extensions/bluebubbles/src/monitor.test.ts
Normal file
1357
extensions/bluebubbles/src/monitor.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user