import { Composer, type Context } from "grammy"; import type { Document } from "grammy/types"; import { parseChatExport } from "../../libs/telegram-export.js"; import { commandOf } from "../../libs/telegram-format.js"; import type { HistoryImportService } from "../../services/history-import-service.js"; import { BOT_API_DOWNLOAD_LIMIT, downloadFile } from "../files.js"; const HOW_TO = "Как загрузить старую переписку:\n" + "1. Telegram Desktop → меню чата → «Экспорт истории чата».\n" + "2. Сними галочки с фото, видео и файлов, формат — «Машиночитаемый JSON».\n" + "3. Пришли мне result.json с подписью /import — в этот чат или мне в личку."; const TOO_BIG = "Файл больше 20 МБ — Telegram не даёт ботам скачивать такие. " + "Экспортируй без медиа или загрузи на сервере: node dist/scripts/import-history.js /app/data/result.json"; const isJson = (document: Document): boolean => document.mime_type === "application/json" || (document.file_name?.toLowerCase().endsWith(".json") ?? false); /** Импорт экспорта чата из Telegram Desktop. Подключать только за фильтром администратора. */ export const createImportHandlers = (historyImportService: HistoryImportService) => { const composer = new Composer(); const runImport = async (ctx: Context, document: Document | undefined) => { if (!document || !isJson(document)) { await ctx.reply(HOW_TO); return; } if (document.file_size !== undefined && document.file_size > BOT_API_DOWNLOAD_LIMIT) { await ctx.reply(TOO_BIG); return; } try { const bytes = await downloadFile(ctx.api, document.file_id); const exported = parseChatExport(JSON.parse(new TextDecoder().decode(bytes))); // В группе принимаем только экспорт этой же группы; в личке — грузим туда, откуда экспорт. if (ctx.chat!.type !== "private" && exported.chatId !== ctx.chat!.id) { await ctx.reply(`Это экспорт другого чата («${exported.title}»). Пришли его в тот чат или мне в личку.`); return; } const result = historyImportService.importChat(exported); const lines = [ `Загрузил «${exported.title}»: новых сообщений ${result.inserted} из ${result.total}, участников +${result.newMembers}.`, `В истории сейчас ${result.stored} сообщений.`, result.total > result.historyLimit ? `Храню не больше ${result.historyLimit} последних — остальное не влезло. Нужно больше — подними HISTORY_LIMIT.` : "", ]; await ctx.reply(lines.filter(Boolean).join("\n")); } catch (error) { console.error("Ошибка импорта истории:", error); await ctx.reply(`Не получилось загрузить: ${error instanceof Error ? error.message : "неизвестная ошибка"}`); } }; // Файл с подписью /import. grammY command() подписи не видит — разбираем через commandOf. composer.on("message:document", async (ctx, next) => { if (commandOf(ctx.msg, ctx.me.username)?.name !== "import") return next(); await runImport(ctx, ctx.msg.document); }); // /import ответом на уже отправленный файл — или без файла, чтобы получить инструкцию. composer.command("import", (ctx) => runImport(ctx, ctx.msg.reply_to_message?.document)); return composer; };