feat(bot): add Telegram media ingest into /data/media

This commit is contained in:
yiekheng 2026-05-09 17:23:59 +08:00
parent d9a5f5a5e2
commit 2ed436ef0e

View File

@ -0,0 +1,89 @@
import { mkdir, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
import { createHash } from "node:crypto";
import { mediaFiles } from "@cmbot/db";
import { newMediaPath, absoluteMediaPath } from "@cmbot/shared";
import { db } from "../db.js";
import { env } from "../env.js";
import { logger } from "../logger.js";
export type IngestInput = {
operatorId: string;
filenameOriginal: string;
mimeType: string;
buffer: Buffer;
};
export type IngestResult = {
mediaId: string;
storagePath: string;
};
export async function ingestMediaBuffer(input: IngestInput): Promise<IngestResult> {
const sha256 = createHash("sha256").update(input.buffer).digest("hex");
const storagePath = newMediaPath(input.filenameOriginal);
const absolute = absoluteMediaPath(storagePath, env.MEDIA_DIR);
await mkdir(dirname(absolute), { recursive: true });
await writeFile(absolute, input.buffer);
const [row] = await db
.insert(mediaFiles)
.values({
operatorId: input.operatorId,
filenameOriginal: input.filenameOriginal,
mimeType: input.mimeType,
sizeBytes: input.buffer.byteLength,
sha256,
storagePath,
})
.returning({ id: mediaFiles.id });
logger.info(
{ mediaId: row!.id, sizeBytes: input.buffer.byteLength, sha256 },
"media: ingested",
);
return { mediaId: row!.id, storagePath };
}
/**
* Download a Telegram file by file_id and ingest it. Returns the new media row.
*/
export async function ingestTelegramFile(
operatorId: string,
apiBase: string,
botToken: string,
fileId: string,
defaultFilename: string,
mimeType: string,
): Promise<IngestResult> {
// 1. getFile — Telegram returns a file_path
const getFileUrl = `${apiBase}/bot${botToken}/getFile?file_id=${encodeURIComponent(fileId)}`;
const getFileRes = await fetch(getFileUrl);
if (!getFileRes.ok) {
throw new Error(`Telegram getFile failed: ${getFileRes.status} ${getFileRes.statusText}`);
}
const getFileJson = (await getFileRes.json()) as {
ok: boolean;
result?: { file_path?: string };
};
if (!getFileJson.ok || !getFileJson.result?.file_path) {
throw new Error("Telegram getFile: missing file_path in response");
}
// 2. Download bytes
const downloadUrl = `${apiBase}/file/bot${botToken}/${getFileJson.result.file_path}`;
const dl = await fetch(downloadUrl);
if (!dl.ok) {
throw new Error(`Telegram file download failed: ${dl.status} ${dl.statusText}`);
}
const buffer = Buffer.from(await dl.arrayBuffer());
// The Telegram-side filename can be missing; fall back to defaultFilename.
const filename = getFileJson.result.file_path.split("/").pop() ?? defaultFilename;
return ingestMediaBuffer({
operatorId,
filenameOriginal: filename,
mimeType,
buffer,
});
}