Files
ros 0e19c84a6d Make bot multi-tenant with colorful welcome and per-owner isolation
The bot is now public: anyone connects it to their own Telegram Business
account and gets antidelete for their own private chats. On connection
(business_connection enabled) and on /start it sends a colorful welcome
describing features, how to connect, and limitations.

- connections.js: resolve a connection's owner from business_connection_id
  (memo -> DB -> getBusinessConnection). Business messages have no outgoing
  flag, so the owner's own messages are filtered by comparing from.id; if
  the owner can't be resolved the message is not cached.
- Strict per-owner isolation: captures scoped by owner_id; the panel shows
  each user only their own feed; notifications go to the owner's chat.
- db.js: multi-tenant schema (connections table; messages keyed by
  (conn_id, chat_id, msg_id); captures/counts scoped by owner_id).
- media.js: key cached-media filenames by (connId, chatId, msgId) to
  prevent one tenant overwriting another's encrypted media.
- panel.js: drop the owner-only barrier; /start sends welcome + own feed.
- config.js: OWNER_ID is now optional (service logs only, grants no access).
- Docs: README/.env.example rewritten for the multi-tenant model and the
  shared-key privacy caveat.
- Stop tracking .claude/settings.local.json; restore the Hcrgram/ ignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 22:16:44 +03:00

11 KiB
Raw Permalink Blame History

telegrambusiness

Telegram-бот на Node.js, работающий через официальное бизнес-подключение Telegram (Business API). Бот доступен всем: любой человек подключает его к своему бизнес-аккаунту как чат-бота и получает сохранение того, что собеседник удалил:

  • antidelete — сообщения (и медиа), которые собеседник удалил в личке.

Бот многопользовательский. При подключении он присылает владельцу красочное приветствие с описанием функций, а все перехваты приходят этому же владельцу в личку. Данные строго изолированы: каждый видит только свои перехваты. Наружу бот ничего не пишет, слеш-команд в чужих чатах нет.

Без сессии, по правилам Telegram. Проект не хранит строку сессии и не использует MTProto-юзербот. Работает исключительно через штатную функцию «Telegram для бизнеса» — обычный бот от @BotFather с включённым Business Mode. Это соответствует ToS.

⚠️ Дисклеймер. Инструмент сохраняет входящие в личке того, кто подключил бота к своему аккаунту. Оператор бота, у которого лежат data/ и ENCRYPTION_KEY, технически имеет доступ к расшифровке. Разворачивайте и используйте ответственно, уважая приватность собеседников.

Как устроено (важно понимать до запуска)

Telegram в бизнес-событии удаления (deleted_business_messages) присылает только id сообщений — без текста и без медиа. Поэтому «сохранить удалённое» реактивно невозможно: бот заранее кэширует каждое входящее в личке, а при удалении достаёт его из своего кэша. То же с медиа — файл качается в момент прихода, после удаления ссылка мертва.

Отсюда следствия:

  • тексты и метаданные пишутся в локальную БД (node:sqlite), зашифрованы (AES-256-GCM);
  • медиа лежат отдельными зашифрованными файлами в data/media/;
  • всё чистится по RETENTION_DAYS.

Чего этот подход НЕ умеет

  • Одноразовые медиа («просмотр один раз», самоуничтожающиеся) перехватить невозможно: сервер Bot API отбрасывает такие сообщения ещё до доставки боту. Это ограничение платформы, обойти его без юзербота нельзя — а от юзербота мы отказались сознательно.
  • Медиа больше ~20 МБ не скачиваются: у Bot API (getFile) лимит на размер файла. Такое медиа сохранится только как пометка о типе.

Установка

Нужен Node.js ≥ 22.5 (для встроенного node:sqlite).

npm install
cp .env.example .env

Заполните .env:

  1. BOT_TOKEN — новый бот у @BotFather.
  2. ENCRYPTION_KEY — сгенерируйте: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))".
  3. OWNER_ID — необязательно, только для служебных логов оператора. Доступа к чужим данным не даёт (изоляция по владельцу), можно не задавать.

Включите боту режим бизнеса — этого достаточно, дальше его подключит каждый пользователь сам:

  1. @BotFather/mybots → выберите бота → Bot SettingsBusiness ModeEnable.

Запуск:

npm run dev

Дальше это делает любой пользователь для своего аккаунта: Настройки → Telegram для бизнеса → Чат-боты → добавить бота и разрешить управление сообщениями. Сразу после подключения бот пришлёт в личку приветствие. Когда собеседник удалит сообщение, придёт уведомление с кнопкой «Открыть». Команда /start в любой момент откроет приветствие и личную ленту перехватов.

npm run dev — с автоперезапуском, npm start — обычный запуск.

Как добавить модуль

Модуль — файл в src/modules/. Загрузчик обходит папку рекурсивно; файлы на _ игнорируются. Объявляйте только нужные хуки:

// src/modules/keywords.js
export default {
  name: 'keywords',
  description: 'Уведомлять, если во входящем есть ключевое слово',

  async onMessage(msg, ctx) {
    if (!msg.isPrivateIncoming) return;
    if (/срочно/i.test(msg.text ?? '')) {
      await ctx.capture('deleted', {           // переиспользуем ленту перехватов
        ownerId: msg.ownerId,                  // чья это лента (изоляция)
        ownerChatId: msg.ownerChatId,          // куда уведомлять
        chatId: msg.chatId,
        senderId: msg.senderId,
        sender: msg.sender,
        text: msg.text,
      });
    }
  },
};

Хук получает нормализованное сообщение с уже разрешённым владельцем подключения: { connId, ownerId, ownerChatId, chatId, msgId, senderId, sender, text, media, isPrivateIncoming }.

Хук Когда вызывается
setup(_, ctx) Один раз при старте
onMessage(msg, ctx) Новое бизнес-сообщение (business_message)
onEdited(msg, ctx) Правка (edited_business_message)
onDeleted(event, ctx) Удаление; event.connId, event.ownerId, event.ownerChatId, event.chatId, event.msgIds

ctx даёт: api (grammY bot.api), registry и capture(kind, data) — сохранить перехват под data.ownerId и уведомить владельца в data.ownerChatId. Ошибка в одном модуле не роняет остальные (registry.jsrunHook).

Устройство

src/
  index.js               сборка: бот + модули + чистка
  config.js              .env, проверка на старте
  core/
    crypto.js            AES-256-GCM (шифр текста и медиа)
    db.js                node:sqlite: кэш входящих + лента перехватов
    media.js             тип медиа, скачать через getFile + шифровать
    loader.js            обход src/modules
    registry.js          реестр модулей, runHook
    logger.js
  business/
    events.js            подписка business_message/edited/deleted/connection → модули
    connections.js       резолвер владельца по business_connection_id
    context.js           ctx.capture — мост модуль↔БД↔бот
  modules/
    cache.js             кэширует входящие (основа antidelete)
    antidelete.js        удаление → достать из кэша → перехват
  bot/
    panel.js             бот: приветствие, инлайн-панель, лента, выдача медиа
    welcome.js           текст красочного приветствия (/start и подключение)

Поток: подключение (business_connection) → запоминаем владельца и шлём приветствие → входящее (business_message) → cache кэширует текст и качает медиа под этим подключением → при удалении (deleted_business_messages) antidelete поднимает из кэша по (connId, chatId, msgId)ctx.capture пишет в БД под ownerId и шлёт уведомление в ownerChatId → панель показывает владельцу только его записи и отдаёт расшифрованный файл.

Что стоит учесть

  • Многопользовательский режим. Бота подключают к себе разные люди; данные строго изолированы по владельцу (owner_id), в панели каждый видит только своё.
  • У бизнес-сообщений нет флага «исходящее», поэтому своё сообщение владельца отсекаем сравнением from.id с владельцем подключения (business_connection_id → владелец, см. connections.js). Если владельца распознать не удалось, сообщение не кэшируется.
  • Перехватываются только входящие в личных диалогах (см. isPrivateIncoming в events.js).
  • Id сообщения в Bot API уникален только внутри чата, поэтому в БД составной ключ (conn_id, chat_id, msg_id).
  • Если владелец отключит бизнес-подключение в настройках Telegram, перехват для него приостановится (бот залогирует это по апдейту business_connection).
  • CACHE_MEDIA=1 качает медиа каждого входящего, чтобы восстанавливать удалённые картинки. Это ест диск — при =0 останется только текст и пометка о типе медиа.
  • Ключ шифрования — в .env, один на все подключения. Потеряете ключ — расшифровать хранилище нельзя.
  • Хранилище локальное; data/ в .gitignore. Схема БД сменилась на мультитенантную — при обновлении со старой версии удалите прежнюю data/ перед первым запуском.