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,132 @@
"use client";
import { type ColumnDef } from "@tanstack/react-table";
import {
MoreHorizontal,
Pencil,
Trash2,
Power,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import type { ChannelRow } from "@/lib/telegram/admin-queries";
interface ChannelColumnsProps {
onEdit: (channel: ChannelRow) => void;
onToggleActive: (id: string) => void;
onDelete: (id: string) => void;
}
export function getChannelColumns({
onEdit,
onToggleActive,
onDelete,
}: ChannelColumnsProps): ColumnDef<ChannelRow, unknown>[] {
return [
{
accessorKey: "title",
header: "Channel",
cell: ({ row }) => (
<div className="flex flex-col">
<span className="font-medium">{row.original.title}</span>
<span className="text-xs text-muted-foreground">
ID: {row.original.telegramId}
</span>
</div>
),
enableHiding: false,
},
{
accessorKey: "type",
header: "Type",
cell: ({ row }) => (
<Badge
variant="outline"
className={
row.original.type === "SOURCE"
? "bg-blue-500/10 text-blue-600 border-blue-500/20"
: "bg-purple-500/10 text-purple-600 border-purple-500/20"
}
>
{row.original.type}
</Badge>
),
},
{
accessorKey: "isActive",
header: "Status",
cell: ({ row }) => (
<Badge variant={row.original.isActive ? "default" : "secondary"}>
{row.original.isActive ? "Active" : "Disabled"}
</Badge>
),
},
{
id: "accounts",
header: "Accounts",
cell: ({ row }) => (
<span className="text-sm text-muted-foreground">
{row.original.accountCount}
</span>
),
},
{
id: "packages",
header: "Packages",
cell: ({ row }) => (
<span className="text-sm text-muted-foreground">
{row.original.packageCount}
</span>
),
},
{
accessorKey: "createdAt",
header: "Created",
cell: ({ row }) => (
<span className="text-sm text-muted-foreground">
{new Date(row.original.createdAt).toLocaleDateString()}
</span>
),
},
{
id: "actions",
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onEdit(row.original)}>
<Pencil className="mr-2 h-3.5 w-3.5" />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onToggleActive(row.original.id)}
>
<Power className="mr-2 h-3.5 w-3.5" />
{row.original.isActive ? "Disable" : "Enable"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => onDelete(row.original.id)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-3.5 w-3.5" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
enableHiding: false,
},
];
}