- Added inline button functionality to the `/configs` command, allowing users to request connection strings for their configurations without displaying them in chat history. - Introduced a constant message for unlinked Telegram accounts to improve user understanding of the linking process. - Updated the handling of configuration messages to include inline buttons for better user interaction and experience.
17 KiB
Telegram Bot
Telegram-бот — второй канал доставки (presentation-адаптер) поверх той же Application-логики,
что и REST API. Он не содержит бизнес-правил: обработчики апдейтов вызывают те же CQRS-команды/запросы
(ICommand/IQuery через собственный ISender), что и веб. Бизнес-инварианты живут в домене.
Возможности (реализовано)
- Мои конфиги —
/configsприсылает по сообщению на конфиг (метка/локация, протокол, статус) с inline-кнопкой «🔗 Показать ссылку»; connection string приходит отдельным сообщением только по нажатию (не светится в списке/истории чата без явного действия пользователя). Только текстовая ссылка, без 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}+ inline-кнопка «🔗 Показать ссылку» (кроме отозванных — там кнопки нет). Если конфигов нет — «У вас пока нет конфигов.» - Нажатие кнопки → callback
cfg:link:{configId}→ бот вызываетGetConfigLinkQuery(тот же, что эндпоинт/api/configs/{id}/link) от текущего пользователя и присылает connection string отдельным сообщением. Ссылка не дублируется никуда до явного нажатия. 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.