diff --git a/.env.example b/.env.example index 149b843..20e3ce9 100644 --- a/.env.example +++ b/.env.example @@ -43,6 +43,9 @@ Telegram__BotUsername=PnvPanelBot # и получают уведомления о запросах на активацию. Узнать id: @userinfobot. # Не связано с сид-админом выше — привязка Telegram к сид-админу делается вручную в UI. Telegram__AdminTelegramUserIds=123456789 +# Прокси для запросов к Bot API (обычно socks5://[user:pass@]host:port) — на случай, если Telegram +# заблокирован напрямую с сети сервера. Пусто (по умолчанию) — без прокси, прямое подключение. +# Telegram__ProxyUrl=socks5://127.0.0.1:39372 # ── ASP.NET Core ────────────────────────────────────────────────────────── ASPNETCORE_ENVIRONMENT=Production diff --git a/backend/src/PnvPanel.Api/Program.cs b/backend/src/PnvPanel.Api/Program.cs index 5ebc504..b65e001 100644 --- a/backend/src/PnvPanel.Api/Program.cs +++ b/backend/src/PnvPanel.Api/Program.cs @@ -59,11 +59,26 @@ builder.Services.AddSingleton(); // реальный HTTP-вызов через неё никогда не происходит (все вызывающие места сами проверяют BotToken). builder.Services.AddSingleton(sp => { - var options = sp.GetRequiredService>(); - var token = options.Value.BotToken; - return new TelegramBotClient(string.IsNullOrWhiteSpace(token) + var options = sp.GetRequiredService>().Value; + var token = string.IsNullOrWhiteSpace(options.BotToken) ? "0:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - : token); + : options.BotToken; + + if (string.IsNullOrWhiteSpace(options.ProxyUrl)) + return new TelegramBotClient(token); + + // Прокси (обычно socks5://) для запросов к Bot API — на случай, если Telegram недоступен напрямую + // с сети сервера. Пусто (по умолчанию) — без прокси, прямое подключение (см. .env.example). + var proxyUri = new Uri(options.ProxyUrl); + var proxy = new WebProxy(proxyUri); + if (!string.IsNullOrEmpty(proxyUri.UserInfo)) + { + var credentials = proxyUri.UserInfo.Split(':', 2); + proxy.Credentials = new NetworkCredential(credentials[0], credentials.Length > 1 ? credentials[1] : string.Empty); + } + + var handler = new SocketsHttpHandler { Proxy = proxy, UseProxy = true }; + return new TelegramBotClient(token, new HttpClient(handler)); }); // Scoped — зависит от IIdentityService (scoped), не Singleton. builder.Services.AddScoped(); diff --git a/backend/src/PnvPanel.Api/Telegram/TelegramBotHostedService.cs b/backend/src/PnvPanel.Api/Telegram/TelegramBotHostedService.cs index 2c52479..6189042 100644 --- a/backend/src/PnvPanel.Api/Telegram/TelegramBotHostedService.cs +++ b/backend/src/PnvPanel.Api/Telegram/TelegramBotHostedService.cs @@ -17,6 +17,8 @@ public sealed class TelegramBotHostedService( ILogger logger) : BackgroundService { + private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(30); + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) @@ -33,13 +35,32 @@ public sealed class TelegramBotHostedService( logger.LogInformation("Telegram bot starting (long polling)..."); - try + // ReceiveAsync может упасть с сетевой ошибкой (Telegram API недоступен/заблокирован) — + // это НЕ должно ронять весь хост (BackgroundServiceExceptionBehavior.StopHost по умолчанию + // убивает всё приложение при необработанном исключении в BackgroundService). Панель обязана + // работать без бота, поэтому ловим, логируем и переподключаемся с паузой (см. CLAUDE.md). + while (!stoppingToken.IsCancellationRequested) { - await botClient.ReceiveAsync(updateHandler, receiverOptions, stoppingToken); - } - catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) - { - // Штатная остановка вместе с приложением. + try + { + await botClient.ReceiveAsync(updateHandler, receiverOptions, stoppingToken); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + // Штатная остановка вместе с приложением. + } + catch (Exception ex) + { + logger.LogError(ex, "Telegram bot polling failed, retrying in {Delay}", RetryDelay); + try + { + await Task.Delay(RetryDelay, stoppingToken); + } + catch (OperationCanceledException) + { + // Остановка приложения во время паузы перед повтором. + } + } } } } diff --git a/backend/src/PnvPanel.Infrastructure/Telegram/TelegramOptions.cs b/backend/src/PnvPanel.Infrastructure/Telegram/TelegramOptions.cs index 3d9e490..3f8efd8 100644 --- a/backend/src/PnvPanel.Infrastructure/Telegram/TelegramOptions.cs +++ b/backend/src/PnvPanel.Infrastructure/Telegram/TelegramOptions.cs @@ -8,6 +8,9 @@ public sealed class TelegramOptions public string? BotUsername { get; init; } public string PublicSiteUrl { get; init; } = string.Empty; + /// Прокси для запросов к Bot API (обычно socks5://[user:pass@]host:port). Пусто — без прокси. + public string? ProxyUrl { get; init; } + /// Telegram id админов (через запятую) — авторизуют админ-кнопки в боте. public string? AdminTelegramUserIds { get; init; }