# 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_`) + смену пароля в настройках на сайте. - 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/?start=link_`. Фронт рисует QR из `deepLink` сам (`qrcode.react`) — бэкенд картинку не генерирует. 2. Пользователь открывает бота по ссылке → `/start link_`. 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/?start=login_` → бот по `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_` | Привязка аккаунта по токену | нет | | `/start login_` | Подтверждение 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.