Files
PnvPanel/docs/telegram-bot.md
T

151 lines
13 KiB
Markdown
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.
# Telegram Bot
Telegram-бот — **второй канал доставки** (presentation-адаптер) поверх той же Application-логики,
что и REST API. Он не содержит бизнес-правил: команды бота вызывают те же CQRS-команды/запросы
(`ICommand/IQuery`), что и веб. Бизнес-инварианты живут в домене, а не в обработчиках бота.
## Возможности
1. **Ссылка на сайт** — кнопка/команда, открывающая веб-панель (при желании — с одноразовым
deep-link авто-входом для уже привязанного пользователя).
2. **Мои конфиги** — список VPN-конфигов пользователя (протокол, локация, трафик, срок, статус),
ссылка-подписка и QR по каждому. Доступно только привязанному аккаунту.
3. **Авторизация через Telegram (passwordless)** — вход на сайт без пароля: подтверждение входа
в боте. Требует предварительной **привязки Telegram** к аккаунту.
4. **Админ: обработка запросов активации** — админ (по Telegram id из env) получает уведомление
о запросе активации с комментарием заявителя и жмёт «Активировать / Отклонить» прямо в боте.
5. **DM-уведомления пользователю** — если Telegram привязан, бот шлёт личные уведомления о ключевых
событиях: «аккаунт активирован», «конфиг отозван админом», «вы заблокированы».
6. **Восстановление пароля** — если пароль забыт, привязанный пользователь через бота получает
одноразовую ссылку на страницу задания нового пароля (или входит passwordless и меняет пароль в
настройках). Без привязки Telegram восстановление делает только админ.
> **Скоуп бота в MVP — просмотр (read-only) по конфигам.** Создание/ротация/отзыв конфигов — только
> на сайте. Полное самообслуживание в боте (создание/отзыв) — в backlog.
## Размещение в архитектуре
- Бот работает **в том же процессе**, что и API, как `BackgroundService`
(`TelegramBotHostedService`) — это укладывается в требование «фронт+бек в одном контейнере».
- Транспорт с Telegram: **long polling** для MVP (не требует публичного webhook-URL, проще в
одиночном контейнере). Webhook — опциональная альтернатива для прод-нагрузки (тогда — секретный
токен заголовка для верификации).
- Библиотека — **Telegram.Bot** (де-факто стандарт для C#).
- Код бота лежит в `PnvPanel.Api/Telegram/` (хендлеры апдейтов, построители клавиатур,
форматтеры сообщений). Обращения к домену — **только** через собственный `ISender`.
`Telegram.Bot` не проникает в Application/Domain.
```
Telegram ──updates──► TelegramBotHostedService (Api)
│ ISender.Send(command/query) // свой диспетчер
Application (те же хендлеры, что и REST)
```
## Модель данных (добавления)
- `AppUser.TelegramUserId : long?` — id пользователя Telegram (уникальный, nullable до привязки).
- `AppUser.TelegramUsername : string?`, `AppUser.TelegramLinkedAt : DateTimeOffset?`.
- `TelegramLinkToken` — короткоживущий одноразовый токен привязки (`token`, `userId`, `expiresAt`, `consumedAt`).
- `TelegramLoginRequest` — запрос passwordless-входа: `id/nonce`, `status`
(`Pending/Approved/Rejected/Expired/Consumed`), `userId?` (после подтверждения), `createdAt`, `expiresAt`.
Подробности полей — в [domain-model.md](domain-model.md).
## Флоу 1 — Привязка Telegram к аккаунту
Предусловие: пользователь уже вошёл на сайте (изначально аккаунт создаётся с username+пароль).
1. На сайте «Привязать Telegram» → `POST /api/auth/telegram/link-token``{ deepLink }`
вида `https://t.me/<bot>?start=link_<token>` (+ QR). Токен короткоживущий, одноразовый.
2. Пользователь открывает бота по ссылке → `/start link_<token>`.
3. Бот берёт `from.id` (Telegram user id), валидирует токен (`LinkTelegramCommand`), проставляет
`TelegramUserId`/`TelegramUsername`/`TelegramLinkedAt`, гасит токен.
4. Бот подтверждает: «Аккаунт привязан». Сайт узнаёт об успехе (поллинг статуса или SignalR).
Инварианты: один `TelegramUserId` ↔ один аккаунт; повторная привязка требует отвязки; токен
нельзя переиспользовать и он истекает.
## Флоу 2 — Passwordless-вход через бота
Предусловие: Telegram уже привязан к аккаунту.
1. На сайте «Войти через Telegram» → `POST /api/auth/telegram/login-request`
`{ requestId, deepLink, qr, expiresAt }`. Сайт начинает ждать результат (поллинг
`GET /api/auth/telegram/login-request/{requestId}` или событие SignalR).
2. Пользователь открывает `https://t.me/<bot>?start=login_<nonce>` → бот по `from.id` находит
привязанный аккаунт и показывает inline-кнопки **«Подтвердить вход / Отклонить»**
(с деталями: время, IP/устройство инициатора — для защиты от фишинга).
3. Подтверждение (`ApproveTelegramLoginCommand`) → запрос переходит в `Approved`, привязывается к `userId`.
4. Сайт (по поллингу/SignalR) получает результат: бэкенд выпускает **стандартные JWT**
access в теле ответа, refresh в httpOnly cookie. Запрос помечается `Consumed`.
Если Telegram **не привязан** — passwordless-вход невозможен (бот предлагает сперва привязать
аккаунт). Регистрация целиком через Telegram — вне MVP (см. backlog).
## Флоу 3 — Просмотр конфигов в боте
1. Привязанный пользователь: `/configs` или кнопка «Мои конфиги».
2. Бот вызывает `GetMyConfigsQuery` (тот же, что и веб) от имени `AppUser`, найденного по `TelegramUserId`.
3. Ответ — список с трафиком/сроком/статусом; по каждому конфигу — inline-кнопки «Ссылка», «QR».
QR отдаётся как изображение (генерация на сервере).
## Флоу 4 — Обработка активации админом в боте
1. Пользователь отправляет запрос активации (сайт: `POST /api/activation/request { comment }`);
доменное событие `ActivationRequested`.
2. Бот шлёт сообщение каждому админу (Telegram id из `AdminSeed__TelegramUserIds`) с username/комментарием
заявителя и inline-кнопками **«✅ Активировать / ❌ Отклонить»**.
3. Нажатие → `ApproveActivationCommand`/`RejectActivationCommand` (те же, что на сайте) → пользователь
активируется, ему уходит realtime-пуш `userActivated`, админам обновляется сообщение (решение зафиксировано).
4. Действие доступно только Telegram id из списка админов; проверка — на стороне бота перед вызовом команды.
## Команды и клавиатуры
| Команда / кнопка | Действие | Требует привязки |
| ---------------------- | -------------------------------------------------------------- | ---------------- |
| `/start` | Приветствие + меню (Открыть сайт / Мои конфиги / Войти) | нет |
| `/start link_<t>` | Привязка аккаунта по токену | нет |
| `/start login_<n>` | Подтверждение passwordless-входа | да |
| «Открыть сайт» | Ссылка на веб-панель (опц. одноразовый auto-login deep link) | нет / да |
| `/configs` | Список конфигов | да |
| `/login` | Инициировать/подтвердить вход | да |
| `/resetpassword` | Одноразовая ссылка на смену пароля (восстановление) | да |
| `/unlink` | Отвязать Telegram от аккаунта | да |
| `/help` | Справка | нет |
| «Активировать/Отклонить» | (admin) решение по запросу активации | админ по env |
| `/requests` | (admin) список ожидающих запросов активации | админ по env |
## Безопасность
- Токены привязки и nonce входа: высокоэнтропийные, **короткоживущие** (≈25 мин), **одноразовые**.
- Подтверждение входа показывает контекст (время/устройство) — защита от несанкционированных запросов.
- Верификация источника апдейтов: webhook — секретный заголовок; long polling — прямой канал к Bot API по TLS.
- Rate-limiting на создание login/link-запросов и на команды бота.
- Токен бота — секрет (env/secret-store), в логи не попадает; апдейты логируются без чувствительных данных.
- Passwordless-вход выпускает те же JWT/refresh, что и обычный — единые правила сессий и ротации.
- Альтернатива боту для веб-входа — официальный **Telegram Login Widget** (HMAC-подпись данных
ботом, верификация на бэке). Оставлено как опция; основной путь — подтверждение в боте.
## Конфигурация
```jsonc
"Telegram": {
"BotToken": "…", // секрет (env/secret-store)
"BotUsername": "PnvPanelBot",
"Mode": "LongPolling", // или "Webhook"
"WebhookUrl": null,
"WebhookSecret": null,
"PublicSiteUrl": "https://panel.example.com"
}
```
Telegram id администраторов задаются отдельно — `AdminSeed__TelegramUserIds` (см.
[`.env.example`](../.env.example)); именно они авторизуют админ-кнопки в боте и получают
уведомления о запросах активации.
Сообщения бота локализованы (**RU/EN**) по языку пользователя, синхронно с настройкой языка в вебе.
Строго типизированные `IOptions<TelegramOptions>` с валидацией на старте; при отсутствии
`BotToken` бот не стартует (панель работает без него).