Переписать проект в личный MTProto-юзербот

Смена архитектуры с бизнес-бота на Bot API (команды со слешем) на
личный юзербот на GramJS/MTProto с управлением через компаньон-бота.

Возможности:
- antidelete — кэш входящих в личке, восстановление удалённых «у всех»
- onetime — перехват «просмотр один раз» фото/видео до истечения ссылки
- локальное хранилище node:sqlite + шифрование AES-256-GCM, чистка по сроку
- компаньон-бот (grammY): инлайн-панель, лента с пагинацией, выдача медиа,
  доступ только владельцу по OWNER_ID

Устройство:
- core/ — crypto, db, media, loader, registry, logger
- userbot/ — MTProto-клиент из строки сессии, подписка на события
- modules/ — фичи как модули с хуками setup/onMessage/onEdited/onDeleted
- bot/ — компаньон-бот (единственный UI)
- scripts/login.js — генерация строки сессии

Удалён старый код Bot API (commands/, handlers/, middlewares/, dispatcher,
store). Обновлены README и .env.example. Hcrgram/ (сторонний референс)
добавлен в .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 19:16:52 +03:00
parent e92e0df644
commit 15a03a77fd
40 changed files with 1845 additions and 922 deletions
+122
View File
@@ -0,0 +1,122 @@
# telegrambusiness
Личный Telegram-**юзербот** на Node.js + MTProto (GramJS). Работает от вашего аккаунта и сохраняет то, что Bot API недоступно:
- **antidelete** — сообщения, которые собеседник удалил в личке;
- **onetime** — одноразовые («просмотр один раз») фото и видео.
Всё управление и все перехваты приходят в **компаньон-бота** — отдельного бота от [@BotFather](https://t.me/BotFather), в личку владельца. Наружу юзербот ничего не пишет, слеш-команд в чатах нет.
> ⚠️ **Дисклеймер.** Инструмент для личного использования на **своём** аккаунте и своей переписке. Юзерботы нарушают ToS Telegram и аккаунт могут заблокировать — держите его на отдельном номере, не на основном. Одноразовые медиа отправляются с расчётом на удаление; сохраняя их, вы берёте ответственность на себя.
## Как устроено (важно понимать до запуска)
MTProto в событии удаления присылает **только id сообщений** — без текста и без медиа. Поэтому «сохранить удалённое» реактивно невозможно: юзербот **заранее кэширует каждое входящее** в личке, а при удалении достаёт его из своего кэша. То же с медиа — файл качается **в момент прихода**, после удаления ссылка мертва.
Отсюда следствия:
- тексты и метаданные пишутся в локальную БД (`node:sqlite`), **зашифрованы** (AES-256-GCM);
- медиа лежат отдельными **зашифрованными** файлами в `data/media/`;
- всё чистится по `RETENTION_DAYS`.
## Установка
Нужен Node.js ≥ 22.5 (для встроенного `node:sqlite`).
```bash
npm install
cp .env.example .env
```
Заполните `.env`:
1. **API_ID / API_HASH** — [my.telegram.org](https://my.telegram.org) → API development tools.
2. **BOT_TOKEN** — новый бот у [@BotFather](https://t.me/BotFather) (это и есть панель).
3. **OWNER_ID** — ваш id (напишите [@userinfobot](https://t.me/userinfobot)).
4. **ENCRYPTION_KEY** — сгенерируйте: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`.
Затем одноразовый вход в аккаунт (телефон, код, при наличии — пароль 2FA):
```bash
npm run login
```
Скрипт напечатает строку сессии — вставьте её в `.env` как `SESSION=...` и запускайте:
```bash
npm run dev
```
Откройте своего бота, нажмите **Start** — появится панель. При удалении сообщения собеседником или получении одноразового медиа бот пришлёт уведомление с кнопкой «Открыть».
`npm run dev` — с автоперезапуском, `npm start` — обычный запуск.
## Как добавить модуль
Модуль — файл в `src/modules/`. Загрузчик обходит папку рекурсивно; файлы на `_` игнорируются. Объявляйте только нужные хуки:
```js
// src/modules/keywords.js
export default {
name: 'keywords',
description: 'Уведомлять, если во входящем есть ключевое слово',
async onMessage(message, ctx) {
if (message.out) return; // не своё
if (/срочно/i.test(message.text ?? '')) {
await ctx.capture('deleted', { // переиспользуем ленту перехватов
chatId: message.chatId,
senderId: message.senderId,
sender: (await message.getSender().catch(() => null))?.username ?? '',
text: message.text,
});
}
},
};
```
| Хук | Когда вызывается |
| --- | --- |
| `setup(_, ctx)` | Один раз при старте |
| `onMessage(message, ctx)` | Новое сообщение (`NewMessage`) |
| `onEdited(message, ctx)` | Правка (`EditedMessage`) |
| `onDeleted(event, ctx)` | Удаление; `event.deletedIds` — массив id |
`ctx` даёт: `client` (GramJS), `me`, `registry` и `capture(kind, data)` — сохранить перехват и уведомить владельца. Ошибка в одном модуле не роняет остальные ([registry.js](src/core/registry.js) — `runHook`).
## Устройство
```
src/
index.js сборка: юзербот + бот + модули + чистка
config.js .env, проверка на старте
core/
crypto.js AES-256-GCM (шифр текста и медиа)
db.js node:sqlite: кэш входящих + лента перехватов
media.js тип медиа, детект one-time, скачать+шифровать
loader.js обход src/modules
registry.js реестр модулей, runHook
logger.js
userbot/
client.js MTProto-клиент из строки сессии
events.js подписка NewMessage/Edited/Deleted → модули
context.js ctx.capture — мост модуль↔БД↔бот
modules/
cache.js кэширует входящие (основа antidelete)
antidelete.js удаление → достать из кэша → перехват
onetime.js one-time медиа → скачать → перехват
bot/
panel.js компаньон-бот: инлайн-панель, лента, выдача медиа
scripts/
login.js генерация строки сессии
```
Поток: входящее → `cache`/`onetime` (кэш и/или скачивание) → при удалении `antidelete` поднимает из кэша → `ctx.capture` пишет в БД и шлёт уведомление → панель показывает и отдаёт расшифрованный файл.
## Что стоит учесть
- Перехватываются только **входящие в личных диалогах**. Группы, каналы и свои сообщения — нет (см. `isPrivateIncoming` в [events.js](src/userbot/events.js)).
- «Удалить у себя» на стороне собеседника юзерботу не приходит — ловится только «удалить у всех».
- `CACHE_MEDIA=1` качает медиа каждого входящего, чтобы восстанавливать удалённые картинки. Это ест диск — при `=0` останется только текст и пометка о типе медиа.
- Ключ шифрования — в `.env`. Потеряете ключ — расшифровать хранилище нельзя.
- Хранилище локальное; `data/` в `.gitignore`.
```