Files
PnvPanel/docs/telegram-bot.md
T
Leonid Pershin cdd67f8e2b
CI / Backend (build + test) (push) Successful in 1m15s
CI / Frontend (lint + typecheck + build) (push) Successful in 30s
Refactor environment configuration and update documentation for MVP status
- Removed deprecated Telegram user ID configuration from `.env.example` and added a new setting for admin Telegram user IDs.
- Updated `CLAUDE.md` to reflect the current MVP status, detailing completed features and testing coverage.
- Enhanced `README.md` with quick start instructions for Docker setup and clarified project status.
- Revised API design documentation to include updated error handling and request/response structures.
- Improved frontend documentation to outline the project structure and technologies used.
2026-07-02 14:12:50 +03:00

175 lines
16 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` через собственный `ISender`), что и веб. Бизнес-инварианты живут в домене.
## Возможности (реализовано)
1. **Мои конфиги**`/configs` присылает текстовый список (метка/локация, протокол, статус) —
**без ссылок и QR** в самом боте; за ссылкой/QR пользователь идёт на сайт. Доступно только
привязанному аккаунту.
2. **Авторизация через Telegram (passwordless)** — вход на сайт без пароля: инициируется на сайте,
подтверждается в боте кнопками «Подтвердить/Отклонить». Требует предварительной привязки Telegram.
3. **Админ: обработка запросов активации** — админ (по Telegram id из `Telegram__AdminTelegramUserIds`)
получает сообщение о каждом запросе активации с именем и комментарием заявителя, жмёт
«✅ Активировать / ❌ Отклонить» прямо в сообщении. `/requests` показывает все ожидающие запросы по требованию.
4. **DM-уведомления пользователю** (если Telegram привязан): активация аккаунта, блокировка,
принудительный отзыв конфига админом.
5. **Отвязка**`/unlink`.
**Не реализовано / backlog:**
- Ссылки/QR/подписка в самом боте (только текстовый список конфигов).
- Отдельная команда `/resetpassword` с одноразовой ссылкой — восстановление пароля сейчас идёт
только через обычный passwordless-вход (`/start login_<n>`) + смену пароля в настройках на сайте.
- Webhook-транспорт — только long polling, конфигурации режима/URL в коде нет.
- Регистрация нового аккаунта из бота (только привязка существующего).
- Полное самообслуживание (создание/ротация/отзыв конфигов) — бот **read-only** по конфигам.
## Размещение в архитектуре
- Бот работает **в том же процессе**, что и API, как `BackgroundService` (`TelegramBotHostedService`,
`PnvPanel.Api/Telegram/`) — условие «фронт+бек в одном контейнере».
- Транспорт — **только long polling** (`ITelegramBotClient.ReceiveAsync`). Webhook рассматривался на
этапе планирования, но не реализован: `TelegramOptions` (`Infrastructure/Telegram/TelegramOptions.cs`)
содержит только `BotToken`, `BotUsername`, `PublicSiteUrl`, `AdminTelegramUserIds` — полей
`Mode`/`WebhookUrl`/`WebhookSecret` в коде нет.
- Библиотека — **Telegram.Bot**. Каждый апдейт обрабатывается в своём DI-scope (`PnvBotUpdateHandler`,
как HTTP-запрос — свежие scoped-сервисы на апдейт).
- Обращения к домену — **только** через `ISender`. `Telegram.Bot` не проникает в Application/Domain.
- Если `Telegram:BotToken` не задан — `TelegramBotHostedService.ExecuteAsync` сразу возвращается,
бот не стартует, панель работает без него (лог `Telegram__BotToken не задан — бот не стартует.`).
```
Telegram ──updates──► TelegramBotHostedService → PnvBotUpdateHandler (Api/Telegram/)
│ ISender.Send(command/query) // свой диспетчер, свой DI-scope на апдейт
Application (те же хендлеры, что и REST)
```
## Модель данных (добавления)
- `AppUser.TelegramUserId : long?`, `TelegramUsername : string?`, `TelegramLinkedAt : DateTimeOffset?`.
- `TelegramLinkToken` — короткоживущий одноразовый токен привязки (`token`, `userId`, `expiresAt`, `consumedAt`).
- `TelegramLoginRequest` — запрос passwordless-входа: `id`, `status`
(`Pending/Approved/Rejected/Expired/Consumed`), `userId?` (после подтверждения), `context?`
(IP инициатора, собирается, но **в текст подтверждения в боте не выводится** — известный TODO),
`createdAt`, `expiresAt`.
Подробности полей — в [domain-model.md](domain-model.md).
## Флоу 1 — Привязка Telegram к аккаунту
Предусловие: пользователь уже вошёл на сайте.
1. На сайте «Привязать Telegram» → `POST /api/auth/telegram/link-token``{ deepLink, expiresAt }`,
`deepLink` вида `https://t.me/<bot>?start=link_<token>`. Фронт рисует QR из `deepLink` сам
(`qrcode.react`) — бэкенд картинку не генерирует.
2. Пользователь открывает бота по ссылке → `/start link_<token>`.
3. Бот берёт `from.id`, вызывает `LinkTelegramCommand(token, telegramUserId, telegramUsername)`
валидирует токен, проставляет `TelegramUserId`/`TelegramUsername`/`TelegramLinkedAt`, гасит токен.
4. Бот отвечает «✅ Telegram успешно привязан к вашему аккаунту» (или текст ошибки). Сайт узнаёт об
успехе поллингом статуса активации/профиля.
Инварианты: один `TelegramUserId` ↔ один аккаунт; токен одноразовый и истекает.
## Флоу 2 — Passwordless-вход через бота
Предусловие: Telegram уже привязан к аккаунту.
1. На сайте «Войти через Telegram» → `POST /api/auth/telegram/login-request`
`{ requestId, deepLink, expiresAt }`. Сайт начинает поллить
`GET /api/auth/telegram/login-request/{requestId}`.
2. Пользователь открывает `https://t.me/<bot>?start=login_<requestId>` → бот по `from.id` находит
привязанный аккаунт (если не найден — просит сначала привязать) и показывает сообщение
«Кто-то пытается войти в PnvPanel через ваш аккаунт. Подтвердить вход?» с инлайн-кнопками
**«✅ Подтвердить вход» / «❌ Отклонить»**.
3. Подтверждение → `ApproveTelegramLoginCommand`/`RejectTelegramLoginCommand` → запрос переходит в
`Approved`/`Rejected`.
4. Сайт по следующему поллингу получает результат: при `Approved``accessToken` в теле,
`refresh` уже пришёл в httpOnly cookie (та же логика cookie, что и обычный логин, включая
`Secure = request.IsHttps`). Запрос помечается `Consumed`.
Если Telegram **не привязан** — бот сразу сообщает «Сначала привяжите Telegram к аккаунту на сайте»,
подтвердить вход невозможно. Регистрация целиком через Telegram — вне MVP.
## Флоу 3 — Просмотр конфигов в боте
1. Привязанный пользователь: `/configs`.
2. Бот вызывает `GetMyConfigsQuery` (тот же, что и веб) от пользователя, найденного по `TelegramUserId`.
3. Ответ — обычное текстовое сообщение, по строке на конфиг:
`• {Label ?? Location} ({Protocol}) — {Status}`. Если конфигов нет — «У вас пока нет конфигов.»
**Ссылок, QR и кнопок здесь нет** — за подключением пользователь идёт на сайт.
## Флоу 4 — Обработка активации админом в боте
1. Пользователь отправляет запрос активации (сайт: `POST /api/activation/request { comment }`) →
`RequestActivationCommandHandler` шлёт SignalR `activationRequested` группе `admins` **и** вызывает
`ITelegramNotifier.NotifyAdminsActivationRequestedAsync` (прямой вызов из хендлера, без диспетчера событий).
2. Каждому админу (по `Telegram__AdminTelegramUserIds`) уходит сообщение с именем и комментарием
заявителя и кнопками **«✅ Активировать / ❌ Отклонить»**.
3. Нажатие → `ApproveActivationCommand`/`RejectActivationCommand` (те же, что на сайте) →
пользователь активируется/отклоняется, ему уходит realtime `userActivated` (только при одобрении) +
Telegram-DM, если привязан; нажавшему админу приходит короткое подтверждение («✅ Пользователь
активирован.»/«❌ Запрос отклонён.») — само сообщение с кнопками не редактируется.
4. Проверка прав — на стороне бота (`TrySetAdminCurrentUserAsync`) перед вызовом команды: Telegram id
должен быть в `Telegram__AdminTelegramUserIds`.
`/requests` — тот же список запросов по требованию (до 10 штук, `Pending`), с теми же кнопками; для
кого он доступен — та же проверка админ-id.
## Команды и клавиатуры
| Команда / кнопка | Действие | Требует привязки |
| -------------------------- | -------------------------------------------------------------- | ----------------- |
| `/start` | Приветствие + справка по командам | нет |
| `/start link_<token>` | Привязка аккаунта по токену | нет |
| `/start login_<requestId>` | Подтверждение passwordless-входа (deep-link с сайта) | да |
| `/configs` | Текстовый список конфигов | да |
| `/unlink` | Отвязать Telegram от аккаунта | да |
| `/help` | Справка (то же сообщение, что `/start`) | нет |
| «✅ Активировать»/«❌ Отклонить» | (admin) решение по конкретному запросу активации | админ по env |
| `/requests` | (admin) список ожидающих запросов активации (до 10) | админ по env |
Любой другой текст → «Не понимаю эту команду. /help — список команд.»
## Безопасность
- Токены привязки и `requestId` входа: высокоэнтропийные, короткоживущие, одноразовые (см.
`TelegramLinkToken`/`TelegramLoginRequest` в [domain-model.md](domain-model.md) — точный TTL не
вынесен в отдельное конфигурируемое значение, см. [tech-stack.md](tech-stack.md)).
- Подтверждение входа **не показывает** контекст (время/IP/устройство) инициатора — поле `Context`
собирается (`CreateLoginRequestCommand`), но в текст сообщения бота не подставляется. Если это
важно для защиты от фишинга — доработка на будущее, не текущее поведение.
- Транспорт — только long polling: прямой канал к Bot API по TLS, без верификации webhook-заголовка
(webhook не реализован).
- `Telegram:BotToken` — секрет (env/secret-store), в логи не попадает; при пустом токене
`TelegramBotClient` конструируется с синтаксической заглушкой вместо падения при старте — реальный
HTTP-вызов всё равно не происходит, т.к. `TelegramBotHostedService` и `TelegramNotifier` сами
проверяют `BotToken` перед использованием клиента.
- Passwordless-вход выпускает те же JWT/refresh, что и обычный (тот же `AuthResult`, та же cookie-логика).
- Явного rate-limit на команды бота нет (в отличие от HTTP-эндпоинтов `/api/auth/*`).
## Конфигурация
Реальные поля `TelegramOptions` (секция `Telegram`):
```jsonc
"Telegram": {
"BotToken": "…", // секрет; пусто = бот не стартует
"BotUsername": "PnvPanelBot", // для deepLink; null/пусто -> deepLink в ответах API тоже null
"AdminTelegramUserIds": "123456789,987654321" // через запятую
// "PublicSiteUrl" — поле есть в TelegramOptions, но нигде не читается (мёртвый код,
// не задавай его — эффекта не будет)
}
```
Переменные окружения — `Telegram__BotToken`, `Telegram__BotUsername`, `Telegram__AdminTelegramUserIds`
(см. [`.env.example`](../.env.example)). Именно они авторизуют админ-кнопки в боте и определяют,
кому слать уведомления о запросах активации — **не** сидируются в БД и не связаны с учёткой
сид-админа (`AdminSeed:*`), это независимый список.
Сообщения бота **не локализованы** по языку пользователя — все тексты на русском независимо от языка
интерфейса на сайте (в отличие от веба, где RU/EN переключаются). Синхронизация языка бота с вебом —
не реализована, backlog.