Files
boltun-ai/src/bot/handlers/import-handlers.ts
yunogasai 510221fd3c Импорт старой переписки из экспорта Telegram Desktop
- /import: прислать result.json с подписью /import в чат или боту в личку (или ответить на файл)
- Парсер экспорта (zod): текст частями, упоминания по имени-ссылке, медиа, служебные сообщения
- Импорт идемпотентен и не перезаписывает живые сообщения и ники
- Экспорт другого чата в группе отклоняется; в личке чат определяется по экспорту
- CLI import-history для файлов больше 20 МБ
- /context показывает ID чата
- Общие downloadFile и MEDIA_LABELS для живых сообщений, фото и импорта

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:18:43 +02:00

75 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
};