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( ITelegramUpdateSource updateSource, ITelegramUpdateHandler updateHandler, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation("Telegram bot polling started"); var offset = await GetStartupOffsetAsync(stoppingToken); while (!stoppingToken.IsCancellationRequested) { try { var updates = await updateSource.GetUpdatesAsync( offset: offset, timeout: 30, allowedUpdates: [UpdateType.Message, UpdateType.CallbackQuery], cancellationToken: stoppingToken); foreach (var update in updates) { try { await updateHandler.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"); } private async Task GetStartupOffsetAsync(CancellationToken stoppingToken) { try { var pending = await updateSource.GetUpdatesAsync( offset: -1, limit: 1, cancellationToken: stoppingToken); if (pending.Length == 0) { return 0; } var startupOffset = pending[^1].Id + 1; logger.LogInformation( "Skipping pending updates through {LastPendingUpdateId}; starting polling from offset {StartupOffset}", pending[^1].Id, startupOffset); return startupOffset; } catch (Exception ex) { logger.LogWarning(ex, "Failed to determine startup offset, continuing from offset 0"); return 0; } } }