feat(bot): add Telegram media ingest into /data/media
This commit is contained in:
parent
d9a5f5a5e2
commit
2ed436ef0e
89
apps/bot/src/media/ingest.ts
Normal file
89
apps/bot/src/media/ingest.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user