- 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.
16 KiB
Telegram Bot
Telegram-бот — второй канал доставки (presentation-адаптер) поверх той же Application-логики,
что и REST API. Он не содержит бизнес-правил: обработчики апдейтов вызывают те же CQRS-команды/запросы
(ICommand/IQuery через собственный ISender), что и веб. Бизнес-инварианты живут в домене.
Возможности (реализовано)
- Мои конфиги —
/configsприсылает текстовый список (метка/локация, протокол, статус) — без ссылок и QR в самом боте; за ссылкой/QR пользователь идёт на сайт. Доступно только привязанному аккаунту. - Авторизация через Telegram (passwordless) — вход на сайт без пароля: инициируется на сайте, подтверждается в боте кнопками «Подтвердить/Отклонить». Требует предварительной привязки Telegram.
- Админ: обработка запросов активации — админ (по Telegram id из
Telegram__AdminTelegramUserIds) получает сообщение о каждом запросе активации с именем и комментарием заявителя, жмёт «✅ Активировать / ❌ Отклонить» прямо в сообщении./requestsпоказывает все ожидающие запросы по требованию. - DM-уведомления пользователю (если Telegram привязан): активация аккаунта, блокировка, принудительный отзыв конфига админом.
- Отвязка —
/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 к аккаунту
Предусловие: пользователь уже вошёл на сайте.
- На сайте «Привязать Telegram» →
POST /api/auth/telegram/link-token→{ deepLink, expiresAt },deepLinkвидаhttps://t.me/<bot>?start=link_<token>. Фронт рисует QR изdeepLinkсам (qrcode.react) — бэкенд картинку не генерирует. - Пользователь открывает бота по ссылке →
/start link_<token>. - Бот берёт
from.id, вызываетLinkTelegramCommand(token, telegramUserId, telegramUsername)— валидирует токен, проставляетTelegramUserId/TelegramUsername/TelegramLinkedAt, гасит токен. - Бот отвечает «✅ Telegram успешно привязан к вашему аккаунту» (или текст ошибки). Сайт узнаёт об успехе поллингом статуса активации/профиля.
Инварианты: один TelegramUserId ↔ один аккаунт; токен одноразовый и истекает.
Флоу 2 — Passwordless-вход через бота
Предусловие: Telegram уже привязан к аккаунту.
- На сайте «Войти через Telegram» →
POST /api/auth/telegram/login-request→{ requestId, deepLink, expiresAt }. Сайт начинает поллитьGET /api/auth/telegram/login-request/{requestId}. - Пользователь открывает
https://t.me/<bot>?start=login_<requestId>→ бот поfrom.idнаходит привязанный аккаунт (если не найден — просит сначала привязать) и показывает сообщение «Кто-то пытается войти в PnvPanel через ваш аккаунт. Подтвердить вход?» с инлайн-кнопками «✅ Подтвердить вход» / «❌ Отклонить». - Подтверждение →
ApproveTelegramLoginCommand/RejectTelegramLoginCommand→ запрос переходит вApproved/Rejected. - Сайт по следующему поллингу получает результат: при
Approved—accessTokenв теле,refreshуже пришёл в httpOnly cookie (та же логика cookie, что и обычный логин, включаяSecure = request.IsHttps). Запрос помечаетсяConsumed.
Если Telegram не привязан — бот сразу сообщает «Сначала привяжите Telegram к аккаунту на сайте», подтвердить вход невозможно. Регистрация целиком через Telegram — вне MVP.
Флоу 3 — Просмотр конфигов в боте
- Привязанный пользователь:
/configs. - Бот вызывает
GetMyConfigsQuery(тот же, что и веб) от пользователя, найденного поTelegramUserId. - Ответ — обычное текстовое сообщение, по строке на конфиг:
• {Label ?? Location} ({Protocol}) — {Status}. Если конфигов нет — «У вас пока нет конфигов.» Ссылок, QR и кнопок здесь нет — за подключением пользователь идёт на сайт.
Флоу 4 — Обработка активации админом в боте
- Пользователь отправляет запрос активации (сайт:
POST /api/activation/request { comment }) →RequestActivationCommandHandlerшлёт SignalRactivationRequestedгруппеadminsи вызываетITelegramNotifier.NotifyAdminsActivationRequestedAsync(прямой вызов из хендлера, без диспетчера событий). - Каждому админу (по
Telegram__AdminTelegramUserIds) уходит сообщение с именем и комментарием заявителя и кнопками «✅ Активировать / ❌ Отклонить». - Нажатие →
ApproveActivationCommand/RejectActivationCommand(те же, что на сайте) → пользователь активируется/отклоняется, ему уходит realtimeuserActivated(только при одобрении) + Telegram-DM, если привязан; нажавшему админу приходит короткое подтверждение («✅ Пользователь активирован.»/«❌ Запрос отклонён.») — само сообщение с кнопками не редактируется. - Проверка прав — на стороне бота (
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.