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