import type { ClawdbotConfig } from "openclaw/plugin-sdk"; import type { PopoConfig, PopoSendResult } from "./types.js"; import { popoRequest, popoUploadRequest, popoDownloadRequest } from "./client.js"; import { normalizePopoTarget, detectReceiverType } from "./targets.js"; import path from "path"; import fs from "fs"; export type DownloadFileResult = { buffer: Buffer; contentType?: string; fileName?: string; }; /** * Download a file from POPO using fileId. */ export async function downloadFilePopo(params: { cfg: ClawdbotConfig; fileId: string; }): Promise { const { cfg, fileId } = params; const popoCfg = cfg.channels?.popo as PopoConfig | undefined; if (!popoCfg) { throw new Error("POPO channel not configured"); } const result = await popoDownloadRequest({ cfg: popoCfg, path: `/im/file/download?fileId=${encodeURIComponent(fileId)}`, }); return { buffer: result.buffer, contentType: result.contentType, }; } export type UploadFileResult = { fileId: string; fileName: string; }; /** * Upload a file to POPO. */ export async function uploadFilePopo(params: { cfg: ClawdbotConfig; file: Buffer | string; fileName: string; fileType?: string; }): Promise { const { cfg, file, fileName, fileType } = params; const popoCfg = cfg.channels?.popo as PopoConfig | undefined; if (!popoCfg) { throw new Error("POPO channel not configured"); } const formData = new FormData(); let fileBuffer: Buffer; if (typeof file === "string") { fileBuffer = fs.readFileSync(file); } else { fileBuffer = file; } const blob = new Blob([fileBuffer as unknown as ArrayBuffer], { type: fileType || "application/octet-stream" }); formData.append("file", blob, fileName); const response = await popoUploadRequest<{ fileId: string; fileName: string }>({ cfg: popoCfg, path: "/im/file/upload", formData, }); if (response.code !== 200 || !response.result) { throw new Error(`POPO file upload failed: ${response.message || "unknown error"}`); } return { fileId: response.result.fileId, fileName: response.result.fileName, }; } /** * Upload an image to POPO. */ export async function uploadImagePopo(params: { cfg: ClawdbotConfig; image: Buffer | string; fileName?: string; }): Promise { const { cfg, image, fileName } = params; let actualFileName = fileName ?? "image.png"; let buffer: Buffer; if (typeof image === "string") { buffer = fs.readFileSync(image); if (!fileName) { actualFileName = path.basename(image); } } else { buffer = image; } // Detect content type from extension const ext = path.extname(actualFileName).toLowerCase(); const contentTypes: Record = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp", }; const contentType = contentTypes[ext] || "image/png"; return uploadFilePopo({ cfg, file: buffer, fileName: actualFileName, fileType: contentType, }); } /** * Send an image message using a fileId. */ export async function sendImagePopo(params: { cfg: ClawdbotConfig; to: string; fileId: string; }): Promise { const { cfg, to, fileId } = params; const popoCfg = cfg.channels?.popo as PopoConfig | undefined; if (!popoCfg) { throw new Error("POPO channel not configured"); } const receiver = normalizePopoTarget(to); if (!receiver) { throw new Error(`Invalid POPO target: ${to}`); } const receiverType = detectReceiverType(receiver); const receiverKey = receiverType === "email" ? "receiver" : "groupId"; const response = await popoRequest<{ msgId?: string }>({ cfg: popoCfg, method: "POST", path: "/im/send-msg", body: { [receiverKey]: receiver, msgType: "image", message: { fileId }, }, }); if (response.code !== 200) { throw new Error(`POPO image send failed: ${response.message || `code ${response.code}`}`); } return { messageId: response.result?.msgId, sessionId: receiver, }; } /** * Send a file message using a fileId. */ export async function sendFilePopo(params: { cfg: ClawdbotConfig; to: string; fileId: string; }): Promise { const { cfg, to, fileId } = params; const popoCfg = cfg.channels?.popo as PopoConfig | undefined; if (!popoCfg) { throw new Error("POPO channel not configured"); } const receiver = normalizePopoTarget(to); if (!receiver) { throw new Error(`Invalid POPO target: ${to}`); } const receiverType = detectReceiverType(receiver); const receiverKey = receiverType === "email" ? "receiver" : "groupId"; const response = await popoRequest<{ msgId?: string }>({ cfg: popoCfg, method: "POST", path: "/im/send-msg", body: { [receiverKey]: receiver, msgType: "file", message: { fileId }, }, }); if (response.code !== 200) { throw new Error(`POPO file send failed: ${response.message || `code ${response.code}`}`); } return { messageId: response.result?.msgId, sessionId: receiver, }; } /** * Helper to detect file type from extension. */ export function detectFileType(fileName: string): "image" | "audio" | "file" { const ext = path.extname(fileName).toLowerCase(); const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"]; const audioExts = [".mp3", ".wav", ".ogg", ".opus", ".m4a", ".aac"]; if (imageExts.includes(ext)) return "image"; if (audioExts.includes(ext)) return "audio"; return "file"; } /** * Upload and send media (image or file) from URL, local path, or buffer. */ export async function sendMediaPopo(params: { cfg: ClawdbotConfig; to: string; mediaUrl?: string; mediaBuffer?: Buffer; fileName?: string; }): Promise { const { cfg, to, mediaUrl, mediaBuffer, fileName } = params; let buffer: Buffer; let name: string; if (mediaBuffer) { buffer = mediaBuffer; name = fileName ?? "file"; } else if (mediaUrl) { if (isLocalPath(mediaUrl)) { // Local file path - read directly const filePath = mediaUrl.startsWith("~") ? mediaUrl.replace("~", process.env.HOME ?? "") : mediaUrl.replace("file://", ""); if (!fs.existsSync(filePath)) { throw new Error(`Local file not found: ${filePath}`); } buffer = fs.readFileSync(filePath); name = fileName ?? path.basename(filePath); } else { // Remote URL - fetch const response = await fetch(mediaUrl); if (!response.ok) { throw new Error(`Failed to fetch media from URL: ${response.status}`); } buffer = Buffer.from(await response.arrayBuffer()); name = fileName ?? (path.basename(new URL(mediaUrl).pathname) || "file"); } } else { throw new Error("Either mediaUrl or mediaBuffer must be provided"); } // Upload the file const fileType = detectFileType(name); const uploadResult = await uploadFilePopo({ cfg, file: buffer, fileName: name, }); // Send based on file type if (fileType === "image") { return sendImagePopo({ cfg, to, fileId: uploadResult.fileId }); } else { return sendFilePopo({ cfg, to, fileId: uploadResult.fileId }); } } /** * Check if a string is a local file path (not a URL). */ function isLocalPath(urlOrPath: string): boolean { if (urlOrPath.startsWith("/") || urlOrPath.startsWith("~") || /^[a-zA-Z]:/.test(urlOrPath)) { return true; } try { const url = new URL(urlOrPath); return url.protocol === "file:"; } catch { return true; } }