mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-02 11:30:23 +00:00
pnpm install --frozen-lockfile pnpm build pnpm check pnpm vitest run extensions/line/src/channel.sendPayload.test.ts extensions/line/src/send.test.ts extensions/line/src/outbound-media.test.ts Co-authored-by: masatohoshino <246810661+masatohoshino@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
111 lines
3.0 KiB
TypeScript
111 lines
3.0 KiB
TypeScript
export type LineOutboundMediaKind = "image" | "video" | "audio";
|
|
|
|
export type LineOutboundMediaResolved = {
|
|
mediaUrl: string;
|
|
mediaKind: LineOutboundMediaKind;
|
|
previewImageUrl?: string;
|
|
durationMs?: number;
|
|
trackingId?: string;
|
|
};
|
|
|
|
type ResolveLineOutboundMediaOpts = {
|
|
mediaKind?: LineOutboundMediaKind;
|
|
previewImageUrl?: string;
|
|
durationMs?: number;
|
|
trackingId?: string;
|
|
};
|
|
|
|
export function validateLineMediaUrl(url: string): void {
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(url);
|
|
} catch {
|
|
throw new Error(`LINE outbound media URL must be a valid URL: ${url}`);
|
|
}
|
|
if (parsed.protocol !== "https:") {
|
|
throw new Error(`LINE outbound media URL must use HTTPS: ${url}`);
|
|
}
|
|
if (url.length > 2000) {
|
|
throw new Error(`LINE outbound media URL must be 2000 chars or less (got ${url.length})`);
|
|
}
|
|
}
|
|
|
|
export function detectLineMediaKind(mimeType: string): LineOutboundMediaKind {
|
|
const normalized = mimeType.toLowerCase();
|
|
if (normalized.startsWith("image/")) {
|
|
return "image";
|
|
}
|
|
if (normalized.startsWith("video/")) {
|
|
return "video";
|
|
}
|
|
if (normalized.startsWith("audio/")) {
|
|
return "audio";
|
|
}
|
|
return "image";
|
|
}
|
|
|
|
function isHttpsUrl(url: string): boolean {
|
|
try {
|
|
return new URL(url).protocol === "https:";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function detectLineMediaKindFromUrl(url: string): LineOutboundMediaKind | undefined {
|
|
try {
|
|
const pathname = new URL(url).pathname.toLowerCase();
|
|
if (/\.(png|jpe?g|gif|webp|bmp|heic|heif|avif)$/i.test(pathname)) {
|
|
return "image";
|
|
}
|
|
if (/\.(mp4|mov|m4v|webm)$/i.test(pathname)) {
|
|
return "video";
|
|
}
|
|
if (/\.(mp3|m4a|aac|wav|ogg|oga)$/i.test(pathname)) {
|
|
return "audio";
|
|
}
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export async function resolveLineOutboundMedia(
|
|
mediaUrl: string,
|
|
opts: ResolveLineOutboundMediaOpts = {},
|
|
): Promise<LineOutboundMediaResolved> {
|
|
const trimmedUrl = mediaUrl.trim();
|
|
if (isHttpsUrl(trimmedUrl)) {
|
|
validateLineMediaUrl(trimmedUrl);
|
|
const previewImageUrl = opts.previewImageUrl?.trim();
|
|
if (previewImageUrl) {
|
|
validateLineMediaUrl(previewImageUrl);
|
|
}
|
|
const mediaKind =
|
|
opts.mediaKind ??
|
|
(typeof opts.durationMs === "number" ? "audio" : undefined) ??
|
|
(opts.trackingId?.trim() ? "video" : undefined) ??
|
|
detectLineMediaKindFromUrl(trimmedUrl) ??
|
|
"image";
|
|
return {
|
|
mediaUrl: trimmedUrl,
|
|
mediaKind,
|
|
...(previewImageUrl ? { previewImageUrl } : {}),
|
|
...(typeof opts.durationMs === "number" ? { durationMs: opts.durationMs } : {}),
|
|
...(opts.trackingId ? { trackingId: opts.trackingId } : {}),
|
|
};
|
|
}
|
|
|
|
try {
|
|
const parsed = new URL(trimmedUrl);
|
|
if (parsed.protocol !== "https:") {
|
|
throw new Error(`LINE outbound media URL must use HTTPS: ${trimmedUrl}`);
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof Error && e.message.startsWith("LINE outbound")) {
|
|
throw e;
|
|
}
|
|
}
|
|
throw new Error("LINE outbound media currently requires a public HTTPS URL");
|
|
}
|