Enhance Telegram bot configuration and error handling
CI / Backend (build + test) (push) Successful in 1m13s
CI / Frontend (lint + typecheck + build) (push) Successful in 29s

- Updated `.env.example` to include a new `Telegram__ProxyUrl` setting for proxy configuration when accessing the Bot API, improving connectivity options.
- Modified `Program.cs` to support proxy settings for the Telegram bot client, allowing for better handling of network restrictions.
- Improved error handling in `TelegramBotHostedService` to ensure the bot can recover from network errors without crashing the application, implementing a retry mechanism with a delay.
- Added `ProxyUrl` property to `TelegramOptions` for better configuration management.
This commit is contained in:
Leonid Pershin
2026-07-02 15:20:03 +03:00
parent 48f620b964
commit 5f005b6496
4 changed files with 52 additions and 10 deletions
+19 -4
View File
@@ -59,11 +59,26 @@ builder.Services.AddSingleton<IRealtimeNotifier, SignalRRealtimeNotifier>();
// реальный HTTP-вызов через неё никогда не происходит (все вызывающие места сами проверяют BotToken).
builder.Services.AddSingleton<ITelegramBotClient>(sp =>
{
var options = sp.GetRequiredService<IOptions<TelegramOptions>>();
var token = options.Value.BotToken;
return new TelegramBotClient(string.IsNullOrWhiteSpace(token)
var options = sp.GetRequiredService<IOptions<TelegramOptions>>().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<ITelegramNotifier, TelegramNotifier>();
@@ -17,6 +17,8 @@ public sealed class TelegramBotHostedService(
ILogger<TelegramBotHostedService> 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)
{
// Остановка приложения во время паузы перед повтором.
}
}
}
}
}