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;
}
}
}