From 29e95f780c4ec1d6b5d356ba0125f24869a3819e Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Mon, 23 Mar 2026 18:27:48 +0100 Subject: [PATCH] feat: support all chat types in channel discovery and enrich bot messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Channel Discovery: - Remove channel/supergroup filter from getAccountChats — all chat types (private, groups, Saved Messages, etc.) are now discoverable as sources - Detect and label the self-chat as "Saved Messages" via getMe - Update channel picker dialog to accept any chat type string Bot Rich Messages: - Enhance package send preview with creator, file count, tags, and source channel info in MarkdownV2 caption - Include tags in new_package subscription notifications - Expand getPendingSendRequest to fetch richer package data Performance: - Reviewed pipeline for many-channel load — getChats pagination fix and per-channel getChat pre-load from prior commit address the main concerns - Channels with no new messages skip in 2-3 API calls Co-Authored-By: Claude Opus 4.6 (1M context) --- bot/src/db/queries.ts | 6 ++++ bot/src/send-listener.ts | 24 +++++++++++-- .../_components/channel-picker-dialog.tsx | 2 +- worker/src/db/queries.ts | 1 + worker/src/tdlib/chats.ts | 35 +++++++++++++------ 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/bot/src/db/queries.ts b/bot/src/db/queries.ts index 12a3969..888e23e 100644 --- a/bot/src/db/queries.ts +++ b/bot/src/db/queries.ts @@ -115,9 +115,15 @@ export async function getPendingSendRequest(requestId: string) { select: { id: true, fileName: true, + fileSize: true, + fileCount: true, + creator: true, + tags: true, + archiveType: true, destChannelId: true, destMessageId: true, previewData: true, + sourceChannel: { select: { title: true, telegramId: true } }, }, }, telegramLink: true, diff --git a/bot/src/send-listener.ts b/bot/src/send-listener.ts index fd06b89..ba3b585 100644 --- a/bot/src/send-listener.ts +++ b/bot/src/send-listener.ts @@ -134,9 +134,22 @@ async function processSendRequest(requestId: string): Promise { throw new Error("No global destination channel configured"); } - // Send preview if available + // Send preview with rich caption if available if (pkg.previewData) { - const caption = `📦 *${pkg.fileName}*\n\nSent from Dragon's Stash`; + const lines: string[] = []; + lines.push(`📦 *${escapeMarkdown(pkg.fileName)}*`); + if (pkg.creator) lines.push(`👤 ${escapeMarkdown(pkg.creator)}`); + if (pkg.fileCount > 0) lines.push(`📁 ${pkg.fileCount} files`); + if (pkg.tags && pkg.tags.length > 0) { + lines.push(`🏷️ ${pkg.tags.map((t: string) => escapeMarkdown(t)).join(", ")}`); + } + if (pkg.sourceChannel) { + lines.push(`📡 Source: ${escapeMarkdown(pkg.sourceChannel.title)}`); + } + lines.push(""); + lines.push("_Sent from Dragon's Stash_"); + + const caption = lines.join("\n"); await sendPhotoMessage(targetUserId, Buffer.from(pkg.previewData), caption); } @@ -189,6 +202,9 @@ async function handleNewPackage(payload: string): Promise { `🔔 New package matching your subscriptions:`, ``, `📦 ${escapeHtml(data.fileName)}${creator}`, + ...(data.tags && data.tags.length > 0 + ? [`🏷️ ${data.tags.map((t: string) => escapeHtml(t)).join(", ")}`] + : []), ``, `Matched: ${patterns.map((p) => `"${escapeHtml(p)}"`).join(", ")}`, ``, @@ -213,3 +229,7 @@ async function handleNewPackage(payload: string): Promise { function escapeHtml(text: string): string { return text.replace(/&/g, "&").replace(//g, ">"); } + +function escapeMarkdown(text: string): string { + return text.replace(/([_*[\]()~`>#+\-=|{}.!\\])/g, "\\$1"); +} diff --git a/src/app/(app)/telegram/_components/channel-picker-dialog.tsx b/src/app/(app)/telegram/_components/channel-picker-dialog.tsx index eee8d58..f7f8391 100644 --- a/src/app/(app)/telegram/_components/channel-picker-dialog.tsx +++ b/src/app/(app)/telegram/_components/channel-picker-dialog.tsx @@ -21,7 +21,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; interface FetchedChannel { chatId: string; title: string; - type: "channel" | "supergroup"; + type: string; isForum: boolean; memberCount: number | null; alreadyLinked: boolean; diff --git a/worker/src/db/queries.ts b/worker/src/db/queries.ts index b86cc5b..177003b 100644 --- a/worker/src/db/queries.ts +++ b/worker/src/db/queries.ts @@ -150,6 +150,7 @@ export async function createPackageWithFiles(input: CreatePackageInput) { packageId: pkg.id, fileName: input.fileName, creator: input.creator ?? null, + tags: input.tags ?? [], }) ); } catch { diff --git a/worker/src/tdlib/chats.ts b/worker/src/tdlib/chats.ts index b85bd56..dc8a192 100644 --- a/worker/src/tdlib/chats.ts +++ b/worker/src/tdlib/chats.ts @@ -16,13 +16,24 @@ export interface TelegramChatInfo { /** * Fetch all chats the account is a member of. * Uses TDLib's getChats to load the chat list, then getChat for details. - * Filters to channels and supergroups only (groups/privates are not useful for ingestion). + * Returns ALL chat types: channels, supergroups, groups, private chats, + * and the special "Saved Messages" (self) chat. */ export async function getAccountChats( client: Client ): Promise { const chats: TelegramChatInfo[] = []; + // Get the current user's ID so we can label Saved Messages + let selfUserId: number | null = null; + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const me = (await client.invoke({ _: "getMe" })) as any; + selfUserId = me.id; + } catch { + log.warn("Failed to get current user via getMe"); + } + // Load ALL chats from the main list by paginating getChats. // TDLib's getChats returns batches — keep calling until it returns // an empty list, which signals all chats have been loaded. @@ -56,6 +67,7 @@ export async function getAccountChats( const chatType = chat.type?._; let type: TelegramChatInfo["type"] = "other"; let isForum = false; + let title = chat.title ?? `Chat ${chatId}`; if (chatType === "chatTypeSupergroup") { // Get supergroup details to check if it's a channel or group @@ -78,17 +90,18 @@ export async function getAccountChats( type = "group"; } else if (chatType === "chatTypePrivate" || chatType === "chatTypeSecret") { type = "private"; + // Label the self-chat as "Saved Messages" + if (selfUserId !== null && chat.type?.user_id === selfUserId) { + title = "Saved Messages"; + } } - // Only include channels and supergroups - if (type === "channel" || type === "supergroup") { - chats.push({ - chatId: BigInt(chatId), - title: chat.title ?? `Chat ${chatId}`, - type, - isForum, - }); - } + chats.push({ + chatId: BigInt(chatId), + title, + type, + isForum, + }); } catch (err) { log.warn({ chatId, err }, "Failed to get chat details, skipping"); } @@ -99,7 +112,7 @@ export async function getAccountChats( log.info( { total: chats.length }, - "Fetched channels/supergroups from Telegram" + "Fetched all chats from Telegram" ); return chats;