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.
This commit is contained in:
+3
-2
@@ -36,9 +36,10 @@ Roles__DefaultUserMaxConfigs=3
|
||||
|
||||
# ── Telegram-бот ──────────────────────────────────────────────────────────
|
||||
# Если BotToken пуст — бот не стартует, панель работает без него. Транспорт — только long polling
|
||||
# (webhook не реализован, отдельного режима/URL для него нет).
|
||||
# (webhook не реализован, отдельного режима/URL для него нет). Username бота для диплинков
|
||||
# (кнопка «Привязать Telegram»/QR) панель узнаёт сама через Bot API (getMe) — задавать его отдельно
|
||||
# не нужно и негде (раньше был Telegram__BotUsername — убран, чтобы не ломать диплинк опечаткой/пробелом).
|
||||
Telegram__BotToken=
|
||||
Telegram__BotUsername=PnvPanelBot
|
||||
# Telegram id(ы) администраторов (через запятую). Дают права админа в боте (кнопки активации)
|
||||
# и получают уведомления о запросах на активацию. Узнать id: @userinfobot.
|
||||
# Не связано с сид-админом выше — привязка Telegram к сид-админу делается вручную в UI.
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using PnvPanel.Api.Common;
|
||||
using PnvPanel.Application.Auth;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Application.Telegram;
|
||||
using PnvPanel.Domain.Telegram;
|
||||
using PnvPanel.Infrastructure.Telegram;
|
||||
|
||||
namespace PnvPanel.Api.Endpoints;
|
||||
|
||||
@@ -25,16 +24,14 @@ public static class TelegramEndpoints
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateLinkToken(
|
||||
ISender sender, IOptions<TelegramOptions> options, CancellationToken cancellationToken)
|
||||
ISender sender, ITelegramBotInfo botInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await sender.Send(new CreateLinkTokenCommand(), cancellationToken);
|
||||
if (!result.IsSuccess)
|
||||
return result.ToHttpResult();
|
||||
|
||||
var botUsername = options.Value.BotUsername;
|
||||
var deepLink = string.IsNullOrWhiteSpace(botUsername)
|
||||
? null
|
||||
: $"https://t.me/{botUsername}?start=link_{result.Value.Token}";
|
||||
var botUsername = await botInfo.GetUsernameAsync(cancellationToken);
|
||||
var deepLink = botUsername is null ? null : $"https://t.me/{botUsername}?start=link_{result.Value.Token}";
|
||||
|
||||
return Results.Ok(new LinkTokenResponseDto(deepLink, result.Value.ExpiresAt));
|
||||
}
|
||||
@@ -46,17 +43,15 @@ public static class TelegramEndpoints
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateLoginRequest(
|
||||
HttpRequest request, ISender sender, IOptions<TelegramOptions> options, CancellationToken cancellationToken)
|
||||
HttpRequest request, ISender sender, ITelegramBotInfo botInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var context = request.HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
var result = await sender.Send(new CreateLoginRequestCommand(context), cancellationToken);
|
||||
if (!result.IsSuccess)
|
||||
return result.ToHttpResult();
|
||||
|
||||
var botUsername = options.Value.BotUsername;
|
||||
var deepLink = string.IsNullOrWhiteSpace(botUsername)
|
||||
? null
|
||||
: $"https://t.me/{botUsername}?start=login_{result.Value.RequestId}";
|
||||
var botUsername = await botInfo.GetUsernameAsync(cancellationToken);
|
||||
var deepLink = botUsername is null ? null : $"https://t.me/{botUsername}?start=login_{result.Value.RequestId}";
|
||||
|
||||
return Results.Ok(new TelegramLoginRequestResponseDto(result.Value.RequestId, deepLink, result.Value.ExpiresAt));
|
||||
}
|
||||
|
||||
@@ -85,6 +85,8 @@ builder.Services.AddSingleton<ITelegramBotClient>(sp =>
|
||||
});
|
||||
// Scoped — зависит от IIdentityService (scoped), не Singleton.
|
||||
builder.Services.AddScoped<ITelegramNotifier, TelegramNotifier>();
|
||||
// Singleton — кэширует username бота (getMe) на весь процесс, не из ручного env (см. TelegramBotInfo).
|
||||
builder.Services.AddSingleton<ITelegramBotInfo, TelegramBotInfo>();
|
||||
builder.Services.AddSingleton<PnvBotUpdateHandler>();
|
||||
builder.Services.AddHostedService<TelegramBotHostedService>();
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
using PnvPanel.Infrastructure.Telegram;
|
||||
using Telegram.Bot;
|
||||
|
||||
namespace PnvPanel.Api.Telegram;
|
||||
|
||||
/// <summary>Кэширует username бота на время жизни процесса (getMe не меняется, повторный запрос не нужен).</summary>
|
||||
internal sealed class TelegramBotInfo(ITelegramBotClient botClient, IOptions<TelegramOptions> options) : ITelegramBotInfo
|
||||
{
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private string? _cachedUsername;
|
||||
|
||||
public async Task<string?> GetUsernameAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cachedUsername is not null)
|
||||
return _cachedUsername;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.Value.BotToken))
|
||||
return null;
|
||||
|
||||
await _lock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (_cachedUsername is not null)
|
||||
return _cachedUsername;
|
||||
|
||||
var me = await botClient.GetMe(cancellationToken);
|
||||
_cachedUsername = me.Username;
|
||||
return _cachedUsername;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Telegram недоступен/бот не отвечает — деплинк просто не покажем вызывающей стороне.
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace PnvPanel.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Метаданные самого бота (не пользователя). Реализация — в Api (нужен ITelegramBotClient),
|
||||
/// аналогично ITelegramNotifier.
|
||||
/// </summary>
|
||||
public interface ITelegramBotInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Username бота для диплинков (https://t.me/{username}?start=...), получен через Bot API
|
||||
/// (getMe), не из ручной конфигурации — так деплинк не сломается из-за опечатки/пробела в env.
|
||||
/// Null, если BotToken не задан или Telegram недоступен.
|
||||
/// </summary>
|
||||
Task<string?> GetUsernameAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ public sealed class TelegramOptions
|
||||
public const string SectionName = "Telegram";
|
||||
|
||||
public string? BotToken { get; init; }
|
||||
public string? BotUsername { get; init; }
|
||||
public string PublicSiteUrl { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Прокси для запросов к Bot API (обычно socks5://[user:pass@]host:port). Пусто — без прокси.</summary>
|
||||
|
||||
+2
-1
@@ -39,7 +39,8 @@ rate-limit'ом (`RateLimiting:AuthPermitLimit`, по умолчанию 20 за
|
||||
| POST | `/api/auth/telegram/login-request` | — | `{ requestId, deepLink, expiresAt }` |
|
||||
| GET | `/api/auth/telegram/login-request/{id}` | — | см. ниже |
|
||||
|
||||
`deepLink` — `null`, если `Telegram:BotUsername` не настроен (бот не привязан к инстансу), иначе
|
||||
`deepLink` — `null`, если `Telegram:BotToken` не настроен или Bot API недоступен (username бота
|
||||
панель получает сама через `getMe`, см. [telegram-bot.md](telegram-bot.md#конфигурация)), иначе
|
||||
`https://t.me/<bot>?start=link_<token>` / `?start=login_<requestId>`. **QR backend не рендерит** —
|
||||
фронт строит QR из `deepLink` сам (`qrcode.react`).
|
||||
|
||||
|
||||
@@ -157,14 +157,20 @@ Telegram ──updates──► TelegramBotHostedService → PnvBotUpdateHandl
|
||||
```jsonc
|
||||
"Telegram": {
|
||||
"BotToken": "…", // секрет; пусто = бот не стартует
|
||||
"BotUsername": "PnvPanelBot", // для deepLink; null/пусто -> deepLink в ответах API тоже null
|
||||
"ProxyUrl": "socks5://[user:pass@]host:port", // прокси для запросов к Bot API; пусто = без прокси
|
||||
"AdminTelegramUserIds": "123456789,987654321" // через запятую
|
||||
// "PublicSiteUrl" — поле есть в TelegramOptions, но нигде не читается (мёртвый код,
|
||||
// не задавай его — эффекта не будет)
|
||||
}
|
||||
```
|
||||
|
||||
Переменные окружения — `Telegram__BotToken`, `Telegram__BotUsername`, `Telegram__AdminTelegramUserIds`
|
||||
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`](../.env.example)). Именно они авторизуют админ-кнопки в боте и определяют,
|
||||
кому слать уведомления о запросах активации — **не** сидируются в БД и не связаны с учёткой
|
||||
сид-админа (`AdminSeed:*`), это независимый список.
|
||||
|
||||
Reference in New Issue
Block a user