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>
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
# telegrambusiness
|
||||
|
||||
Личный Telegram-бот на Node.js, работающий через **официальное бизнес-подключение** Telegram (Business API). Бот подключается к вашему аккаунту как чат-бот для бизнеса и сохраняет то, что собеседник удалил:
|
||||
Telegram-бот на Node.js, работающий через **официальное бизнес-подключение** Telegram (Business API). Бот **доступен всем**: любой человек подключает его к **своему** бизнес-аккаунту как чат-бота и получает сохранение того, что собеседник удалил:
|
||||
|
||||
- **antidelete** — сообщения (и медиа), которые собеседник удалил в личке.
|
||||
|
||||
Всё управление и все перехваты приходят в этого же бота — в личку владельца. Наружу бот ничего не пишет, слеш-команд в чатах нет.
|
||||
Бот многопользовательский. При подключении он присылает владельцу **красочное приветствие** с описанием функций, а все перехваты приходят этому же владельцу в личку. Данные строго изолированы: **каждый видит только свои** перехваты. Наружу бот ничего не пишет, слеш-команд в чужих чатах нет.
|
||||
|
||||
> ✅ **Без сессии, по правилам Telegram.** Проект **не хранит строку сессии** и не использует MTProto-юзербот. Работает исключительно через штатную функцию «Telegram для бизнеса» — обычный бот от [@BotFather](https://t.me/BotFather) с включённым Business Mode. Это соответствует ToS.
|
||||
|
||||
> ⚠️ **Дисклеймер.** Инструмент для личного использования на **своём** аккаунте и своей переписке. Не применяйте против других людей.
|
||||
> ⚠️ **Дисклеймер.** Инструмент сохраняет входящие в личке того, кто подключил бота к своему аккаунту. Оператор бота, у которого лежат `data/` и `ENCRYPTION_KEY`, технически имеет доступ к расшифровке. Разворачивайте и используйте ответственно, уважая приватность собеседников.
|
||||
|
||||
## Как устроено (важно понимать до запуска)
|
||||
|
||||
@@ -36,13 +36,12 @@ cp .env.example .env
|
||||
Заполните `.env`:
|
||||
|
||||
1. **BOT_TOKEN** — новый бот у [@BotFather](https://t.me/BotFather).
|
||||
2. **OWNER_ID** — ваш id (напишите [@userinfobot](https://t.me/userinfobot)).
|
||||
3. **ENCRYPTION_KEY** — сгенерируйте: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`.
|
||||
2. **ENCRYPTION_KEY** — сгенерируйте: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`.
|
||||
3. **OWNER_ID** — необязательно, только для служебных логов оператора. Доступа к чужим данным не даёт (изоляция по владельцу), можно не задавать.
|
||||
|
||||
Включите боту режим бизнеса и подключите его к аккаунту:
|
||||
Включите боту режим бизнеса — этого достаточно, дальше его подключит каждый пользователь сам:
|
||||
|
||||
1. [@BotFather](https://t.me/BotFather) → `/mybots` → выберите бота → **Bot Settings** → **Business Mode** → **Enable**.
|
||||
2. В приложении Telegram: **Настройки → Telegram для бизнеса → Чат-боты** → добавьте своего бота. Дайте ему право читать/управлять сообщениями.
|
||||
|
||||
Запуск:
|
||||
|
||||
@@ -50,7 +49,7 @@ cp .env.example .env
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Откройте своего бота, нажмите **Start** — появится панель. Когда собеседник удалит сообщение, бот пришлёт уведомление с кнопкой «Открыть».
|
||||
Дальше это делает **любой пользователь** для своего аккаунта: **Настройки → Telegram для бизнеса → Чат-боты** → добавить бота и разрешить управление сообщениями. Сразу после подключения бот пришлёт в личку приветствие. Когда собеседник удалит сообщение, придёт уведомление с кнопкой «Открыть». Команда **/start** в любой момент откроет приветствие и личную ленту перехватов.
|
||||
|
||||
`npm run dev` — с автоперезапуском, `npm start` — обычный запуск.
|
||||
|
||||
@@ -68,6 +67,8 @@ export default {
|
||||
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,
|
||||
@@ -78,16 +79,16 @@ export default {
|
||||
};
|
||||
```
|
||||
|
||||
Хук получает **нормализованное** сообщение: `{ chatId, msgId, senderId, sender, text, media, isPrivateIncoming }`.
|
||||
Хук получает **нормализованное** сообщение с уже разрешённым владельцем подключения: `{ 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.chatId`, `event.msgIds` |
|
||||
| `onDeleted(event, ctx)` | Удаление; `event.connId`, `event.ownerId`, `event.ownerChatId`, `event.chatId`, `event.msgIds` |
|
||||
|
||||
`ctx` даёт: `api` (grammY `bot.api`), `registry` и `capture(kind, data)` — сохранить перехват и уведомить владельца. Ошибка в одном модуле не роняет остальные ([registry.js](src/core/registry.js) — `runHook`).
|
||||
`ctx` даёт: `api` (grammY `bot.api`), `registry` и `capture(kind, data)` — сохранить перехват под `data.ownerId` и уведомить владельца в `data.ownerChatId`. Ошибка в одном модуле не роняет остальные ([registry.js](src/core/registry.js) — `runHook`).
|
||||
|
||||
## Устройство
|
||||
|
||||
@@ -103,22 +104,26 @@ src/
|
||||
registry.js реестр модулей, runHook
|
||||
logger.js
|
||||
business/
|
||||
events.js подписка business_message/edited/deleted → модули
|
||||
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 бот: инлайн-панель, лента, выдача медиа
|
||||
panel.js бот: приветствие, инлайн-панель, лента, выдача медиа
|
||||
welcome.js текст красочного приветствия (/start и подключение)
|
||||
```
|
||||
|
||||
Поток: входящее (`business_message`) → `cache` кэширует текст и качает медиа → при удалении (`deleted_business_messages`) `antidelete` поднимает из кэша → `ctx.capture` пишет в БД и шлёт уведомление → панель показывает и отдаёт расшифрованный файл.
|
||||
Поток: подключение (`business_connection`) → запоминаем владельца и шлём приветствие → входящее (`business_message`) → `cache` кэширует текст и качает медиа под этим подключением → при удалении (`deleted_business_messages`) `antidelete` поднимает из кэша по `(connId, chatId, msgId)` → `ctx.capture` пишет в БД под `ownerId` и шлёт уведомление в `ownerChatId` → панель показывает владельцу только его записи и отдаёт расшифрованный файл.
|
||||
|
||||
## Что стоит учесть
|
||||
|
||||
- Перехватываются только **входящие в личных диалогах**. Свои сообщения — нет (см. `isPrivateIncoming` в [events.js](src/business/events.js)).
|
||||
- Id сообщения в Bot API уникален **только внутри чата**, поэтому в БД составной ключ `(chat_id, msg_id)`.
|
||||
- Если вы отключите бизнес-подключение в настройках Telegram, перехват приостановится (бот залогирует это по апдейту `business_connection`).
|
||||
- **Многопользовательский режим.** Бота подключают к себе разные люди; данные строго изолированы по владельцу (`owner_id`), в панели каждый видит только своё.
|
||||
- У бизнес-сообщений **нет флага «исходящее»**, поэтому своё сообщение владельца отсекаем сравнением `from.id` с владельцем подключения (`business_connection_id` → владелец, см. [connections.js](src/business/connections.js)). Если владельца распознать не удалось, сообщение не кэшируется.
|
||||
- Перехватываются только **входящие в личных диалогах** (см. `isPrivateIncoming` в [events.js](src/business/events.js)).
|
||||
- Id сообщения в Bot API уникален **только внутри чата**, поэтому в БД составной ключ `(conn_id, chat_id, msg_id)`.
|
||||
- Если владелец отключит бизнес-подключение в настройках Telegram, перехват для него приостановится (бот залогирует это по апдейту `business_connection`).
|
||||
- `CACHE_MEDIA=1` качает медиа каждого входящего, чтобы восстанавливать удалённые картинки. Это ест диск — при `=0` останется только текст и пометка о типе медиа.
|
||||
- Ключ шифрования — в `.env`. Потеряете ключ — расшифровать хранилище нельзя.
|
||||
- Хранилище локальное; `data/` в `.gitignore`.
|
||||
- Ключ шифрования — в `.env`, **один на все подключения**. Потеряете ключ — расшифровать хранилище нельзя.
|
||||
- Хранилище локальное; `data/` в `.gitignore`. Схема БД сменилась на мультитенантную — при обновлении со старой версии удалите прежнюю `data/` перед первым запуском.
|
||||
|
||||
Reference in New Issue
Block a user