feat: add Telegram integration with forum topic support and creator tracking

Adds full Telegram ZIP ingestion pipeline: TDLib worker service scans source
channels for archive files, deduplicates by content hash, extracts metadata,
uploads to archive channel, and indexes in Postgres. Forum supergroups are
scanned per-topic with topic names used as creator. Filename-based creator
extraction (e.g. "Mammoth Factory - 2026-01.zip") serves as fallback.

Includes admin UI for managing accounts/channels, simplified account setup
(API credentials via env vars), auth code/password submission dialog,
package browser with creator column, and live ingestion activity tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xCyanGrizzly
2026-02-24 16:02:06 +01:00
parent beb9cfb312
commit b427193d17
70 changed files with 8627 additions and 2 deletions

View File

@@ -0,0 +1,76 @@
import type { Client } from "tdl";
import { config } from "../util/config.js";
import { childLogger } from "../util/logger.js";
const log = childLogger("upload");
export interface UploadResult {
messageId: bigint;
}
/**
* Upload one or more files to a destination Telegram channel.
* For multipart archives, each file is sent as a separate message.
* Returns the message ID of the first uploaded message.
*/
export async function uploadToChannel(
client: Client,
chatId: bigint,
filePaths: string[],
caption?: string
): Promise<UploadResult> {
let firstMessageId: bigint | null = null;
for (let i = 0; i < filePaths.length; i++) {
const filePath = filePaths[i];
const fileCaption =
i === 0 && caption ? caption : undefined;
log.debug(
{ chatId: Number(chatId), filePath, part: i + 1, total: filePaths.length },
"Uploading file to channel"
);
const result = (await client.invoke({
_: "sendMessage",
chat_id: Number(chatId),
input_message_content: {
_: "inputMessageDocument",
document: {
_: "inputFileLocal",
path: filePath,
},
caption: fileCaption
? {
_: "formattedText",
text: fileCaption,
}
: undefined,
},
})) as { id: number };
if (i === 0) {
firstMessageId = BigInt(result.id);
}
// Rate limit delay between uploads
if (i < filePaths.length - 1) {
await sleep(config.apiDelayMs);
}
}
if (firstMessageId === null) {
throw new Error("Upload failed: no messages sent");
}
log.info(
{ chatId: Number(chatId), messageId: Number(firstMessageId), files: filePaths.length },
"Upload complete"
);
return { messageId: firstMessageId };
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}