Files
PnvPanel/docs/telegram-bot.md
T
Leonid Pershin 0d05ff52e9
CI / Backend (build + test) (push) Successful in 1m15s
CI / Frontend (lint + typecheck + build) (push) Successful in 29s
Refactor Telegram bot configuration and deep link handling
- Removed the `BotUsername` property from `TelegramOptions` and updated the `.env.example` to reflect this change, as the bot's username is now dynamically retrieved via the Bot API.
- Introduced `ITelegramBotInfo` to cache the bot's username, improving the handling of deep links in `TelegramEndpoints`.
- Updated API documentation to clarify that the deep link is now dependent on the bot's token and its availability through the Bot API, enhancing clarity for developers.
2026-07-02 15:44:40 +03:00

16 KiB
Raw Blame History

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.

Флоу 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. Сайт по следующему поллингу получает результат: при ApprovedaccessToken в теле, 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 — точный TTL не вынесен в отдельное конфигурируемое значение, см. 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):

"Telegram": {
  "BotToken": "…",              // секрет; пусто = бот не стартует
  "ProxyUrl": "socks5://[user:pass@]host:port", // прокси для запросов к Bot API; пусто = без прокси
  "AdminTelegramUserIds": "123456789,987654321"   // через запятую
  // "PublicSiteUrl" — поле есть в TelegramOptions, но нигде не читается (мёртвый код,
  // не задавай его — эффекта не будет)
}

Username бота для deepLink (кнопка «Привязать Telegram»/QR, ?start=link_<token>/?start=login_<id>) панель получает сама через Bot API (getMe) и кэширует на время жизни процесса (ITelegramBotInfo, Api/Telegram/TelegramBotInfo.cs) — отдельного поля конфигурации для него больше нет (было BotUsername, убрано: опечатка/лишний пробел в env ломали ссылку, а источник истины и так есть в самом Telegram). Если BotToken пуст или getMe не отвечает — deepLink в ответах API будет null.

Переменные окружения — Telegram__BotToken, Telegram__ProxyUrl, Telegram__AdminTelegramUserIds (см. .env.example). Именно они авторизуют админ-кнопки в боте и определяют, кому слать уведомления о запросах активации — не сидируются в БД и не связаны с учёткой сид-админа (AdminSeed:*), это независимый список.

Сообщения бота не локализованы по языку пользователя — все тексты на русском независимо от языка интерфейса на сайте (в отличие от веба, где RU/EN переключаются). Синхронизация языка бота с вебом — не реализована, backlog.