diff --git a/DiplomaticMailBot.Repositories/RegisteredChatRepository.cs b/DiplomaticMailBot.Repositories/RegisteredChatRepository.cs index 6721720..a44db7c 100644 --- a/DiplomaticMailBot.Repositories/RegisteredChatRepository.cs +++ b/DiplomaticMailBot.Repositories/RegisteredChatRepository.cs @@ -162,7 +162,12 @@ public async Task> CreateOrU }; } - public async Task> DeleteAsync(long chatId, string chatAlias, CancellationToken cancellationToken = default) + public async Task> DeleteAsync(long chatId, CancellationToken cancellationToken = default) + { + return await DeleteAsync(chatId, string.Empty, checkAlias: false, cancellationToken); + } + + public async Task> DeleteAsync(long chatId, string chatAlias, bool checkAlias = true, CancellationToken cancellationToken = default) { _logger.LogInformation("Deleting registered chat {ChatId}", chatId); @@ -179,7 +184,7 @@ public async Task> DeleteAsync(long chatId, string chatAlias return new DomainError(EventCode.RegisteredChatNotFound.ToInt(), "Registered chat not found"); } - if (!registeredChat.ChatAlias.EqualsIgnoreCase(chatAlias)) + if (checkAlias && !registeredChat.ChatAlias.EqualsIgnoreCase(chatAlias)) { _logger.LogInformation("Registered chat {ChatId} alias mismatch; expected: {ExpectedAlias}, actual: {ActualAlias}. Won't delete", chatId, diff --git a/DiplomaticMailBot.Services/CommandHandlers/RegisterChatHandler.cs b/DiplomaticMailBot.Services/CommandHandlers/RegisterChatHandler.cs index 7e1f0f3..ba51358 100644 --- a/DiplomaticMailBot.Services/CommandHandlers/RegisterChatHandler.cs +++ b/DiplomaticMailBot.Services/CommandHandlers/RegisterChatHandler.cs @@ -7,6 +7,7 @@ using DiplomaticMailBot.ServiceModels.RegisteredChat; using DiplomaticMailBot.TelegramInterop.Extensions; using DiplomaticMailBot.TelegramInterop.Services; +using Microsoft.Extensions.Logging; using Telegram.Bot; using Telegram.Bot.Types; @@ -14,17 +15,20 @@ namespace DiplomaticMailBot.Services.CommandHandlers; public sealed partial class RegisterChatHandler { + private readonly ILogger _logger; private readonly ITelegramBotClient _telegramBotClient; private readonly TelegramInfoService _telegramInfoService; private readonly RegisteredChatRepository _registeredChatRepository; private readonly PreviewGenerator _previewGenerator; public RegisterChatHandler( + ILogger logger, ITelegramBotClient telegramBotClient, TelegramInfoService telegramInfoService, RegisteredChatRepository registeredChatRepository, PreviewGenerator previewGenerator) { + _logger = logger; _telegramBotClient = telegramBotClient; _telegramInfoService = telegramInfoService; _registeredChatRepository = registeredChatRepository; @@ -111,6 +115,19 @@ await createOrUpdateResult.MatchAsync( } } + public async Task HandleDeregisterExitedChatAsync(User bot, Chat chat, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(bot); + ArgumentNullException.ThrowIfNull(chat); + + _logger.LogDebug("Deregistering chat {ChatId} ({ChatType}, {ChatTitle}) because the bot was kicked or left or restricted", + chat.Id, + chat.Type, + chat.Title); + + await _registeredChatRepository.DeleteAsync(chat.Id, cancellationToken); + } + public async Task HandleDeregisterChatAsync(User bot, Message userCommand, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(bot); @@ -125,7 +142,7 @@ public async Task HandleDeregisterChatAsync(User bot, Message userCommand, Cance { var deregisteredChatAlias = match.Groups["alias"].Value.ToLowerInvariant(); - var deleteResult = await _registeredChatRepository.DeleteAsync(userCommand.Chat.Id, deregisteredChatAlias, cancellationToken); + var deleteResult = await _registeredChatRepository.DeleteAsync(userCommand.Chat.Id, deregisteredChatAlias, cancellationToken: cancellationToken); await deleteResult.MatchAsync( async err => diff --git a/DiplomaticMailBot.Services/TelegramBotService.cs b/DiplomaticMailBot.Services/TelegramBotService.cs index c2c4e94..2270582 100644 --- a/DiplomaticMailBot.Services/TelegramBotService.cs +++ b/DiplomaticMailBot.Services/TelegramBotService.cs @@ -14,7 +14,7 @@ public sealed partial class TelegramBotService { private static readonly ReceiverOptions ReceiverOptions = new() { - AllowedUpdates = [UpdateType.Message], + AllowedUpdates = [UpdateType.Message, UpdateType.MyChatMember], }; private readonly ILogger _logger; @@ -45,6 +45,78 @@ public TelegramBotService( _withdrawMessageHandler = withdrawMessageHandler; } + private async Task HandleMyChatMemberUpdateAsync( + User me, + Update update, + CancellationToken cancellationToken = default) + { + var myChatMember = update.MyChatMember; + if (myChatMember is null) + { + _logger.LogTrace("Ignoring update without my_chat_member"); + return; + } + + var chat = myChatMember.Chat; + + _logger.LogDebug("Handling my_chat_member update for chat {ChatId} ({ChatType}, {ChatTitle})", chat.Id, chat.Type, chat.Title); + + if (update.MyChatMember?.NewChatMember.Status is ChatMemberStatus.Kicked or ChatMemberStatus.Left or ChatMemberStatus.Restricted) + { + await _registerChatHandler.HandleDeregisterExitedChatAsync(me, chat, cancellationToken); + } + } + + private async Task HandleMessageUpdateAsync( + User me, + Update update, + CancellationToken cancellationToken = default) + { + var message = update.Message; + if (message is null) + { + _logger.LogTrace("Ignoring update without message"); + return; + } + + var messageText = message.Text; + if (string.IsNullOrWhiteSpace(messageText)) + { + _logger.LogTrace("Ignoring update with empty message text"); + return; + } + + var match = CommandRegex().Match(messageText); + if (!match.Success) + { + return; + } + + var commandBotUsername = match.Groups["botname"].Value; + + if (!string.IsNullOrWhiteSpace(commandBotUsername) + && !string.IsNullOrWhiteSpace(me.Username) + && !commandBotUsername.Equals(me.Username, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + _logger.LogDebug("Handling command {MessageText}", messageText); + + var handlerTask = messageText switch + { + _ when messageText.StartsWith(BotCommands.ListChats, StringComparison.Ordinal) => _registerChatHandler.HandleListChatsAsync(me, message, cancellationToken), + _ when messageText.StartsWith(BotCommands.RegisterChat, StringComparison.Ordinal) => _registerChatHandler.HandleRegisterChatAsync(me, message, cancellationToken), + _ when messageText.StartsWith(BotCommands.DeregisterChat, StringComparison.Ordinal) => _registerChatHandler.HandleDeregisterChatAsync(me, message, cancellationToken), + _ when messageText.StartsWith(BotCommands.EstablishRelations, StringComparison.Ordinal) => _establishRelationsHandler.HandleEstablishRelationsAsync(me, message, cancellationToken), + _ when messageText.StartsWith(BotCommands.BreakOffRelations, StringComparison.Ordinal) => _breakOffRelationsHandler.HandleBreakOffRelationsAsync(me, message, cancellationToken), + _ when messageText.StartsWith(BotCommands.PutMessage, StringComparison.Ordinal) => _putMessageHandler.HandlePutMessageAsync(me, message, cancellationToken), + _ when messageText.StartsWith(BotCommands.WithdrawMessage, StringComparison.Ordinal) => _withdrawMessageHandler.HandleWithdrawMessageAsync(me, message, cancellationToken), + _ => Task.CompletedTask, + }; + await handlerTask; + } + private async Task HandleUpdateAsync( ITelegramBotClient botClient, Update update, @@ -59,49 +131,18 @@ private async Task HandleUpdateAsync( return; } - var message = update.Message; - if (message is null) - { - _logger.LogTrace("Ignoring update without message"); - return; - } - - var messageText = message.Text; - if (string.IsNullOrWhiteSpace(messageText)) - { - _logger.LogTrace("Ignoring update with empty message text"); - return; - } - - var match = CommandRegex().Match(messageText); - if (!match.Success) - { - return; - } - - var commandBotUsername = match.Groups["botname"].Value; - - if (!string.IsNullOrWhiteSpace(commandBotUsername) - && !string.IsNullOrWhiteSpace(me.Username) - && !commandBotUsername.Equals(me.Username, StringComparison.OrdinalIgnoreCase)) + switch (update.Type) { - return; + case UpdateType.MyChatMember: + await HandleMyChatMemberUpdateAsync(me, update, cancellationToken); + break; + case UpdateType.Message: + await HandleMessageUpdateAsync(me, update, cancellationToken); + break; + default: + _logger.LogDebug("Ignoring update with unknown type: {UpdateType}", update.Type); + break; } - - _logger.LogDebug("Handling command {MessageText}", messageText); - - var handlerTask = messageText switch - { - _ when messageText.StartsWith(BotCommands.ListChats, StringComparison.Ordinal) => _registerChatHandler.HandleListChatsAsync(me, message, cancellationToken), - _ when messageText.StartsWith(BotCommands.RegisterChat, StringComparison.Ordinal) => _registerChatHandler.HandleRegisterChatAsync(me, message, cancellationToken), - _ when messageText.StartsWith(BotCommands.DeregisterChat, StringComparison.Ordinal) => _registerChatHandler.HandleDeregisterChatAsync(me, message, cancellationToken), - _ when messageText.StartsWith(BotCommands.EstablishRelations, StringComparison.Ordinal) => _establishRelationsHandler.HandleEstablishRelationsAsync(me, message, cancellationToken), - _ when messageText.StartsWith(BotCommands.BreakOffRelations, StringComparison.Ordinal) => _breakOffRelationsHandler.HandleBreakOffRelationsAsync(me, message, cancellationToken), - _ when messageText.StartsWith(BotCommands.PutMessage, StringComparison.Ordinal) => _putMessageHandler.HandlePutMessageAsync(me, message, cancellationToken), - _ when messageText.StartsWith(BotCommands.WithdrawMessage, StringComparison.Ordinal) => _withdrawMessageHandler.HandleWithdrawMessageAsync(me, message, cancellationToken), - _ => Task.CompletedTask, - }; - await handlerTask; } catch (Exception e) { diff --git a/DiplomaticMailBot.Tests.Integration/Tests/RegisteredChatRepositoryTests.cs b/DiplomaticMailBot.Tests.Integration/Tests/RegisteredChatRepositoryTests.cs index 799486f..b011f6d 100644 --- a/DiplomaticMailBot.Tests.Integration/Tests/RegisteredChatRepositoryTests.cs +++ b/DiplomaticMailBot.Tests.Integration/Tests/RegisteredChatRepositoryTests.cs @@ -247,7 +247,7 @@ public async Task DeleteAsync_WhenValidInput_DeletesChat(CancellationToken cance timeProvider); // Act - var result = await repository.DeleteAsync(chat.ChatId, chat.ChatAlias, cancellationToken); + var result = await repository.DeleteAsync(chat.ChatId, chat.ChatAlias, cancellationToken: cancellationToken); // Assert Assert.That(result.IsRight, Is.False); @@ -278,7 +278,7 @@ public async Task DeleteAsync_WhenChatNotFound_ReturnsError(CancellationToken ca timeProvider); // Act - var result = await repository.DeleteAsync(123, "test", cancellationToken); + var result = await repository.DeleteAsync(123, "test", cancellationToken: cancellationToken); // Assert Assert.That(result.IsRight, Is.True); @@ -313,7 +313,7 @@ public async Task DeleteAsync_WhenAliasMismatch_ReturnsError(CancellationToken c timeProvider); // Act - var result = await repository.DeleteAsync(chat.ChatId, "wrong_alias", cancellationToken); + var result = await repository.DeleteAsync(chat.ChatId, "wrong_alias", cancellationToken: cancellationToken); // Assert Assert.That(result.IsRight, Is.True); diff --git a/DiplomaticMailBot.Tests.Unit/DomainServices/SlotDateUtilsTests.cs b/DiplomaticMailBot.Tests.Unit/DomainServices/SlotDateUtilsTests.cs index 0b5771c..3ba417e 100644 --- a/DiplomaticMailBot.Tests.Unit/DomainServices/SlotDateUtilsTests.cs +++ b/DiplomaticMailBot.Tests.Unit/DomainServices/SlotDateUtilsTests.cs @@ -55,6 +55,18 @@ public void GetNextAvailableSlotDate_WhenCurrentTimeAfterVoteStart2_ReturnsTomor Assert.That(result, Is.EqualTo(DateOnly.FromDateTime(currentTime.AddDays(1)))); } + [Test] + public void IsVoteGoingOn_WhenNotUtcKind_ThrowsException() + { + // Arrange + var currentTime = new DateTime(2025, 2, 23, 11, 30, 0, DateTimeKind.Local); + var voteStartsAt = new TimeOnly(11, 0); // 11:00 + var voteEndsAt = new TimeOnly(12, 0); // 12:00 + + // Act & Assert + Assert.Throws(() => SlotDateUtils.IsVoteGoingOn(currentTime, voteStartsAt, voteEndsAt)); + } + [Test] public void IsVoteGoingOn_WhenCurrentTimeWithinVotingPeriod_ReturnsTrue() {