Files
TeleWave/backend/src/TeleWave.Infrastructure/Notifications/TelegramPollingBackgroundService.cs
T
Leonid Pershin 2b03e43a83
ci / build-backend (push) Successful in 2m38s
ci / build-frontend (push) Successful in 1m16s
ci / tests (push) Successful in 2m33s
ci / sonar (push) Successful in 3m48s
Add Telegram bot integration and related features
Implemented Telegram bot functionality, including settings management, subscriber tracking, and link generation for user interaction. Updated the backend to support new Telegram-related services and database entities. Enhanced the frontend to display Telegram options and allow users to open a chat with the bot. Localization strings were added for both English and Russian to support the new features. This integration aims to improve user engagement through Telegram notifications and interactions.
2026-07-31 07:58:18 +03:00

122 lines
5.1 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Notifications;
using TeleWave.Domain.Notifications;
namespace TeleWave.Infrastructure.Notifications;
/// <summary>
/// Опрос обновлений (getUpdates) — режим для установки без публичного адреса.
///
/// Служба крутится всегда, а не заводится по настройке: бота включают и выключают в рантайме,
/// и перезапускать контейнер ради этого никто не должен. Пока бот выключен или выбран вебхук,
/// цикл просто дремлет.
/// </summary>
public sealed class TelegramPollingBackgroundService(
IServiceScopeFactory scopeFactory,
ILogger<TelegramPollingBackgroundService> logger
) : BackgroundService
{
/// <summary>Пауза, когда работать нечего: бот выключен, не настроен или выбран вебхук.</summary>
private static readonly TimeSpan IdleDelay = TimeSpan.FromSeconds(30);
/// <summary>Пауза после ошибки — чтобы недоступный прокси не превращался в поток запросов.</summary>
private static readonly TimeSpan ErrorDelay = TimeSpan.FromSeconds(15);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
TimeSpan delay;
try
{
delay = await TickAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
return;
}
catch (Exception exception)
{
logger.LogError(exception, "Ошибка опроса Telegram");
delay = ErrorDelay;
}
if (delay > TimeSpan.Zero)
{
try
{
await Task.Delay(delay, stoppingToken);
}
catch (OperationCanceledException)
{
return;
}
}
}
}
/// <summary>Один заход опроса. Возвращает, сколько ждать перед следующим.</summary>
private async Task<TimeSpan> TickAsync(CancellationToken cancellationToken)
{
await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
var settings = await db.TelegramSettings.FirstOrDefaultAsync(cancellationToken);
if (
settings is null
|| !settings.IsOperational
|| settings.Transport != TelegramTransport.Polling
)
return IdleDelay;
var api = scope.ServiceProvider.GetRequiredService<ITelegramApi>();
var bot = scope.ServiceProvider.GetRequiredService<TelegramBotService>();
IReadOnlyList<TelegramUpdate> updates;
try
{
// Смещение — «следующее за последним разобранным»: так Telegram сам подтверждает приём
// и не отдаёт то же обновление второй раз.
updates = await api.GetUpdatesAsync(
settings,
settings.LastUpdateId + 1,
cancellationToken
);
}
catch (Exception exception) when (exception is not OperationCanceledException)
{
settings.MarkError(exception.Message);
await db.SaveChangesAsync(cancellationToken);
logger.LogWarning(exception, "Telegram недоступен");
return ErrorDelay;
}
settings.MarkContact(DateTimeOffset.UtcNow);
foreach (var update in updates)
{
try
{
await bot.HandleAsync(settings, update, DateTimeOffset.UtcNow, cancellationToken);
}
catch (Exception exception) when (exception is not OperationCanceledException)
{
// Одно кривое обновление не должно останавливать разбор остальных — и, главное,
// не должно повторяться вечно: смещение двигаем в любом случае.
logger.LogError(exception, "Не разобрано обновление {UpdateId}", update.UpdateId);
}
settings.AdvanceUpdateId(update.UpdateId);
}
await db.SaveChangesAsync(cancellationToken);
// Длинный опрос сам держит соединение — своей паузы между заходами не нужно.
return TimeSpan.Zero;
}
}