88 lines
2.7 KiB
C#
88 lines
2.7 KiB
C#
using Telegram.Bot.Types;
|
|
using Telegram.Bot.Types.Enums;
|
|
|
|
namespace GmRelay.Bot.Infrastructure.Telegram;
|
|
|
|
/// <summary>
|
|
/// Long polling loop for Telegram Bot API.
|
|
/// Stateless — all state is in PostgreSQL. Safe to restart at any time.
|
|
/// </summary>
|
|
public sealed class TelegramBotService(
|
|
ITelegramUpdateSource updateSource,
|
|
ITelegramUpdateHandler updateHandler,
|
|
ILogger<TelegramBotService> 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<int> 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;
|
|
}
|
|
}
|
|
}
|