using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; namespace GmRelay.Bot.Infrastructure.Telegram; /// /// Long polling loop for Telegram Bot API. /// Stateless — all state is in PostgreSQL. Safe to restart at any time. /// public sealed class TelegramBotService( ITelegramBotClient bot, UpdateRouter router, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation("Telegram bot polling started"); // Skip any pending updates from before this startup try { var pending = await bot.GetUpdates(offset: -1, limit: 1, cancellationToken: stoppingToken); if (pending.Length > 0) { logger.LogInformation("Skipped {Count} pending update(s)", pending[^1].Id); } } catch (Exception ex) { logger.LogWarning(ex, "Failed to clear pending updates, continuing anyway"); } var offset = 0; while (!stoppingToken.IsCancellationRequested) { try { var updates = await bot.GetUpdates( offset: offset, timeout: 30, allowedUpdates: [UpdateType.Message, UpdateType.CallbackQuery], cancellationToken: stoppingToken); foreach (var update in updates) { try { await router.RouteAsync(update, stoppingToken); } catch (Exception ex) { logger.LogError(ex, "Error handling update {UpdateId}", update.Id); } offset = update.Id + 1; } } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { logger.LogError(ex, "Polling error, retrying in 5s"); await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } } logger.LogInformation("Telegram bot polling stopped"); } }