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

71
src/schemas/telegram.ts Normal file
View File

@@ -0,0 +1,71 @@
import { z } from "zod/v4";
export const paginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(25),
});
export const listPackagesSchema = paginationSchema.extend({
channelId: z.string().optional(),
creator: z.string().optional(),
sortBy: z.enum(["indexedAt", "fileName", "fileSize"]).default("indexedAt"),
order: z.enum(["asc", "desc"]).default("desc"),
});
export const listFilesSchema = paginationSchema.extend({
limit: z.coerce.number().int().min(1).max(500).default(50),
extension: z.string().optional(),
});
export const searchSchema = paginationSchema.extend({
q: z.string().min(1),
searchIn: z.enum(["packages", "files", "both"]).default("both"),
});
export const triggerIngestionSchema = z.object({
accountId: z.string().optional(),
});
// ── Account CRUD ──
export const telegramAccountSchema = z.object({
phone: z
.string()
.min(1, "Phone number is required")
.regex(/^\+?\d[\d\s\-]{6,20}$/, "Invalid phone format (e.g. +31612345678)"),
displayName: z.string().max(64).optional().or(z.literal("")),
});
export type TelegramAccountInput = z.infer<typeof telegramAccountSchema>;
export const submitAuthCodeSchema = z.object({
code: z.string().min(3, "Auth code is required").max(10),
});
export type SubmitAuthCodeInput = z.infer<typeof submitAuthCodeSchema>;
export const submitPasswordSchema = z.object({
password: z.string().min(1, "Password is required"),
});
export type SubmitPasswordInput = z.infer<typeof submitPasswordSchema>;
// ── Channel CRUD ──
export const telegramChannelSchema = z.object({
telegramId: z.coerce.number().int().min(1, "Telegram ID is required"),
title: z.string().min(1, "Title is required").max(256),
type: z.enum(["SOURCE", "DESTINATION"]),
});
export type TelegramChannelInput = z.infer<typeof telegramChannelSchema>;
// ── Account-Channel linking ──
export const linkChannelSchema = z.object({
accountId: z.string().min(1),
channelId: z.string().min(1),
role: z.enum(["READER", "WRITER"]).default("READER"),
});
export type LinkChannelInput = z.infer<typeof linkChannelSchema>;