< Summary

Information
Class: GistBackend.Services.TelegramService
Assembly: GistBackend
File(s): /home/runner/work/the-gist-of-it-sec/the-gist-of-it-sec/backend/GistBackend/Services/TelegramService.cs
Line coverage
89%
Covered lines: 85
Uncovered lines: 10
Coverable lines: 95
Total lines: 185
Line coverage: 89.4%
Branch coverage
63%
Covered branches: 24
Total branches: 38
Branch coverage: 63.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%2290.9%
.cctor()100%11100%
ExecuteAsync()50%2287.5%
OnMessageAsync()62.5%8888.88%
HandleCommandAsync()100%44100%
HandleStartCommandAsync()66.66%66100%
HandleStopCommandAsync()66.66%66100%
OnErrorAsync(...)0%620%
ProcessAllChatsAsync()100%44100%
SendGistsToChatAsync()100%22100%
SendGistToChatAsync()25%5460%
BuildGistMessage(...)100%11100%

File(s)

/home/runner/work/the-gist-of-it-sec/the-gist-of-it-sec/backend/GistBackend/Services/TelegramService.cs

#LineLine coverage
 1using System.Globalization;
 2using GistBackend.Handlers.MariaDbHandler;
 3using GistBackend.Handlers.TelegramBotClientHandler;
 4using GistBackend.Types;
 5using GistBackend.Utils;
 6using Microsoft.Extensions.Hosting;
 7using Microsoft.Extensions.Logging;
 8using Microsoft.Extensions.Options;
 9using Prometheus;
 10using Telegram.Bot.Polling;
 11using Telegram.Bot.Types;
 12using Telegram.Bot.Types.Enums;
 13using static System.Web.HttpUtility;
 14using static GistBackend.Utils.LogEvents;
 15using static GistBackend.Utils.ServiceUtils;
 16
 17namespace GistBackend.Services;
 18
 19public class TelegramService : BackgroundService
 20{
 21    protected CancellationToken? ServiceCancellationToken;
 22    private readonly IMariaDbHandler _mariaDbHandler;
 23    private readonly ITelegramBotClientHandler _telegramBotClientHandler;
 24    private readonly string _appBaseUrl;
 25    private readonly ILogger<TelegramService>? _logger;
 26
 927    public TelegramService(IMariaDbHandler mariaDbHandler,
 928        ITelegramBotClientHandler telegramBotClientHandler,
 929        IOptions<TelegramServiceOptions> options,
 930        ILogger<TelegramService>? logger = null)
 31    {
 932        if (string.IsNullOrWhiteSpace(options.Value.AppBaseUrl))
 033            throw new ArgumentException("App base URL is not set in the options.");
 934        _mariaDbHandler = mariaDbHandler;
 935        _telegramBotClientHandler = telegramBotClientHandler;
 936        _appBaseUrl = options.Value.AppBaseUrl;
 937        _logger = logger;
 938    }
 39
 140    private static readonly BotCommand StartCommand = new("start", "Register to receive messages");
 141    private static readonly BotCommand StopCommand = new("stop", "Unregister to stop receiving messages");
 142    private static readonly List<BotCommand> Commands = [StartCommand, StopCommand];
 343    private static readonly string AvailableCommands = string.Join(", ", Commands.Select(c => $"/{c.Command}"));
 44
 145    private static readonly Gauge ProcessChatsGauge =
 146        Metrics.CreateGauge("process_chats_seconds", "Time spent to process all chats");
 47
 48    protected override async Task ExecuteAsync(CancellationToken ct)
 49    {
 250        ServiceCancellationToken = ct;
 251        _telegramBotClientHandler.StartBotClient([StartCommand, StopCommand], OnMessageAsync, OnErrorAsync, ct);
 252        while (!ct.IsCancellationRequested)
 53        {
 254            var startTime = DateTime.UtcNow;
 455            using (new SelfReportingStopwatch(elapsed => ProcessChatsGauge.Set(elapsed)))
 56            {
 257                await ProcessAllChatsAsync(ct);
 258            }
 259            await DelayUntilNextExecutionAsync(startTime, 1, null, ct);
 60        }
 061    }
 62
 63    protected async Task OnMessageAsync(Message message, UpdateType updateType)
 64    {
 765        if (message.Type != MessageType.Text || message.Text is null)
 066            return;
 767        if (message.Text.StartsWith('/'))
 68        {
 569            await HandleCommandAsync(message);
 70        }
 71        else
 72        {
 273            _logger?.LogInformation(TelegramCommandNotRecognized, "Received message without command: {MessageText}",
 274                message.Text);
 275            await _telegramBotClientHandler.SendMessageAsync(message.Chat.Id,
 276                $"Please use one of the following commands to interact with me: {AvailableCommands}");
 77        }
 778    }
 79
 80    private async Task HandleCommandAsync(Message message)
 81    {
 582        var command = message.Text![1..];
 783        if (command == StartCommand.Command) await HandleStartCommandAsync(message);
 584        else if (command == StopCommand.Command) await HandleStopCommandAsync(message);
 85        else
 86        {
 187            await _telegramBotClientHandler.SendMessageAsync(message.Chat.Id,
 188                $"Unknown command. Please use one of the following commands: {AvailableCommands}");
 89        }
 590    }
 91
 92    private async Task HandleStartCommandAsync(Message message)
 93    {
 294        if (await _mariaDbHandler.IsChatRegisteredAsync(message.Chat.Id, ServiceCancellationToken!.Value))
 95        {
 196            _logger?.LogInformation(StartCommandButAlreadyRegistered,
 197                "Chat {ChatId} tried to register but is already registered", message.Chat.Id);
 198            await _telegramBotClientHandler.SendMessageAsync(message.Chat.Id,
 199                "You are already registered. I will continue to send you gists. Happy reading!");
 100        }
 101        else
 102        {
 1103            _logger?.LogInformation(StartCommandForNewChat, "Registering new chat {ChatId}", message.Chat.Id);
 1104            await _mariaDbHandler.RegisterChatAsync(message.Chat.Id, ServiceCancellationToken!.Value);
 1105            await _telegramBotClientHandler.SendMessageAsync(message.Chat.Id,
 1106                "Welcome to The Gist of IT Sec! I registered your chat. " +
 1107                "I will regularly send you gists of the freshest news of selected outlets.");
 108        }
 2109    }
 110
 111    private async Task HandleStopCommandAsync(Message message)
 112    {
 2113        if (await _mariaDbHandler.IsChatRegisteredAsync(message.Chat.Id, ServiceCancellationToken!.Value))
 114        {
 1115            _logger?.LogInformation(StopCommandForExistingChat, "Deregistering chat {ChatId}", message.Chat.Id);
 1116            await _mariaDbHandler.DeregisterChatAsync(message.Chat.Id, ServiceCancellationToken.Value);
 1117            await _telegramBotClientHandler.SendMessageAsync(message.Chat.Id,
 1118                "Such a shame to see you go. I deregistered you. Goodbye.");
 119        }
 120        else
 121        {
 1122            _logger?.LogInformation(StopCommandButNotRegistered,
 1123                "Chat {ChatId} tried to deregister but is not registered", message.Chat.Id);
 1124            await _telegramBotClientHandler.SendMessageAsync(message.Chat.Id,
 1125                "Seems like you were not registered to begin with. I will not send you gists.");
 126        }
 2127    }
 128
 129    private Task OnErrorAsync(Exception exception, HandleErrorSource source)
 130    {
 0131        _logger?.LogError(UnexpectedTelegramError, exception, "An error occurred in the Telegram service: {Source}",
 0132            source);
 0133        return Task.CompletedTask;
 134    }
 135
 136    private async Task ProcessAllChatsAsync(CancellationToken ct)
 137    {
 2138        var chats = await _mariaDbHandler.GetAllChatsAsync(ct);
 2139        var gistsToSendByGistIdLastSent = new Dictionary<int, List<ConstructedGist>>();
 9140        foreach (var gistId in chats.Select(chat => chat.GistIdLastSent).Distinct())
 141        {
 1142            gistsToSendByGistIdLastSent[gistId] =
 1143                await _mariaDbHandler.GetNextFiveConstructedGistsAsync(gistId, LanguageMode.Original, ct);
 144        }
 10145        foreach (var chat in chats)
 146        {
 3147            await SendGistsToChatAsync(chat.Id, gistsToSendByGistIdLastSent[chat.GistIdLastSent], ct);
 148        }
 2149    }
 150
 151    private async Task SendGistsToChatAsync(long chatId, IEnumerable<ConstructedGist> gists, CancellationToken ct)
 152    {
 51153        foreach (var gist in gists) await SendGistToChatAsync(chatId, gist, ct);
 3154    }
 155
 156    private async Task SendGistToChatAsync(long chatId, ConstructedGist constructedGist, CancellationToken ct)
 157    {
 158        try
 159        {
 15160            _logger?.LogInformation(SendingGistToChat, "Sending gist {GistId} to chat {ChatId}", constructedGist.Id, cha
 15161            var message = BuildGistMessage(constructedGist);
 15162            await _telegramBotClientHandler.SendMessageAsync(chatId, message, ParseMode.Html);
 15163            await _mariaDbHandler.SetGistIdLastSentForChatAsync(chatId, constructedGist.Id, ct);
 15164        }
 0165        catch (Exception ex)
 166        {
 0167            _logger?.LogError(SendingGistToChatFailed, ex,
 0168                "Failed to send gist {GistId} to chat {ChatId}", constructedGist.Id, chatId);
 0169        }
 15170    }
 171
 172    private string BuildGistMessage(ConstructedGist constructedGist)
 173    {
 15174        var updatedString = DateTime
 15175            .ParseExact(constructedGist.Updated, "yyyy-MM-ddTHH:mm:ss.FFFFFFFZ", CultureInfo.InvariantCulture,
 15176                DateTimeStyles.AssumeUniversal).ToString("dd.MM.yyyy HH:mm 'UTC'");
 15177        var gistUrl = $"{_appBaseUrl}/?gist={constructedGist.Id}";
 15178        return $"<b>{HtmlEncode(constructedGist.Title)}</b>\n" +
 15179               $"{HtmlEncode(updatedString)}\n\n" +
 15180               $"{HtmlEncode(constructedGist.Summary)}\n\n" +
 15181               $"Tags: <i>{HtmlEncode(string.Join(", ", constructedGist.Tags))}</i>\n\n" +
 15182               $"{HtmlEncode(constructedGist.FeedTitle)} - {HtmlEncode(constructedGist.Author)}\n" +
 15183               $"More details: {HtmlEncode(gistUrl)}";
 184    }
 185}