feat(telegram): detect and download media messages

Add handlers for voice, audio, photo, video, and document messages.
Each handler emits a ChannelMessage with media attachment metadata.
Implement downloadMedia() to fetch files from Telegram API and save
to the local media cache directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-02-09 09:44:30 +08:00
parent 020d132260
commit 78738e89bf

View file

@ -6,9 +6,13 @@
* - Group chats: only messages that @mention the bot or reply to the bot
*/
import { writeFile, mkdir } from "node:fs/promises";
import { join, extname } from "node:path";
import { v7 as uuidv7 } from "uuid";
import { Bot, GrammyError } from "grammy";
import type { ChannelPlugin, ChannelMessage, ChannelConfigAdapter, ChannelsConfig, DeliveryContext } from "../types.js";
import { markdownToTelegramHtml } from "./telegram-format.js";
import { MEDIA_CACHE_DIR } from "../../shared/paths.js";
/** Telegram account config shape */
interface TelegramAccountConfig {
@ -120,6 +124,74 @@ export const telegramChannel: ChannelPlugin = {
});
});
// Handle media messages (voice, audio, photo, video, document)
const mediaTypes = [
{ filter: "message:voice" as const, getMedia: (msg: any) => ({
type: "audio" as const,
fileId: msg.voice.file_id as string,
mimeType: msg.voice.mime_type as string | undefined,
duration: msg.voice.duration as number | undefined,
})},
{ filter: "message:audio" as const, getMedia: (msg: any) => ({
type: "audio" as const,
fileId: msg.audio.file_id as string,
mimeType: msg.audio.mime_type as string | undefined,
duration: msg.audio.duration as number | undefined,
})},
{ filter: "message:photo" as const, getMedia: (msg: any) => {
// Pick the largest photo size (last in array)
const photos = msg.photo as Array<{ file_id: string }>;
const largest = photos[photos.length - 1]!;
return {
type: "image" as const,
fileId: largest.file_id,
mimeType: "image/jpeg",
};
}},
{ filter: "message:video" as const, getMedia: (msg: any) => ({
type: "video" as const,
fileId: msg.video.file_id as string,
mimeType: msg.video.mime_type as string | undefined,
duration: msg.video.duration as number | undefined,
})},
{ filter: "message:document" as const, getMedia: (msg: any) => ({
type: "document" as const,
fileId: msg.document.file_id as string,
mimeType: msg.document.mime_type as string | undefined,
})},
] as const;
for (const { filter, getMedia } of mediaTypes) {
bot.on(filter, (ctx) => {
const msg = ctx.message;
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
if (isGroup) {
const isReplyToBot = msg.reply_to_message?.from?.is_bot === true;
if (!isReplyToBot) return;
}
const media = getMedia(msg);
const caption = (msg as any).caption as string | undefined;
console.log(`[Telegram] Received ${media.type}: chatId=${msg.chat.id} from=${msg.from?.id} fileId=${media.fileId}`);
onMessage({
messageId: String(msg.message_id),
conversationId: String(msg.chat.id),
senderId: String(msg.from?.id ?? "unknown"),
text: caption ?? "",
chatType: isGroup ? "group" : "direct",
media: {
type: media.type,
fileId: media.fileId,
mimeType: media.mimeType,
duration: (media as any).duration,
caption,
},
});
});
}
// Graceful shutdown on abort
signal.addEventListener("abort", () => {
console.log("[Telegram] Bot stopped");
@ -179,4 +251,27 @@ export const telegramChannel: ChannelPlugin = {
}
},
},
async downloadMedia(fileId: string, accountId: string): Promise<string> {
const bot = bots.get(accountId);
if (!bot) throw new Error(`No Telegram bot for account ${accountId}`);
const file = await bot.api.getFile(fileId);
const filePath = file.file_path;
if (!filePath) throw new Error(`Telegram returned no file_path for fileId=${fileId}`);
const url = `https://api.telegram.org/file/bot${bot.token}/${filePath}`;
const ext = extname(filePath) || ".bin";
const localPath = join(MEDIA_CACHE_DIR, `${uuidv7()}${ext}`);
await mkdir(MEDIA_CACHE_DIR, { recursive: true });
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to download file: HTTP ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
await writeFile(localPath, buffer);
console.log(`[Telegram] Downloaded media: ${filePath}${localPath}`);
return localPath;
},
};