From a832a8a3d54926190503912cd142e7b3dbe379e4 Mon Sep 17 00:00:00 2001 From: shishnk Date: Fri, 12 Apr 2024 00:10:58 +0700 Subject: [PATCH] Have added simple telegram api --- .../Common/Mapping/RegisterMapper.cs | 4 +- .../CreateUserWeatherSubscriptionCommand.cs | 6 +- .../DeleteUserWeatherSubscriptionCommand.cs | 2 +- .../UpdateUserWeatherSubscriptionCommand.cs | 2 +- .../GetUserWeatherSubscriptionsQuery.cs | 2 +- .../UserWeatherSubscriptionDto.cs | 2 + .../Commands/CreateUser/CreateUserCommand.cs | 4 +- .../CreateUser/CreateUserCommandHandler.cs | 6 +- .../DatabaseApp.Application/Users/UserDto.cs | 2 +- .../Core/DatabaseApp.Domain/Models/User.cs | 2 +- .../Repositories/Interfaces.cs | 6 +- .../EventBusExtensions.cs | 12 +- .../CreatedUserIntegrationEventHandler.cs | 38 +++ ...serSubscriptionsIntegrationEventHandler.cs | 54 ++++ ...UserSubscriptionIntegrationEventHandler.cs | 44 ++++ ...UserSubscriptionIntegrationEventHandler.cs | 40 +++ ...UserSubscriptionIntegrationEventHandler.cs | 47 ++++ ...cheRequestUsersIntegrationEventHandler.cs" | 17 +- ...20240411110006_Initialization.Designer.cs} | 10 +- ...ze.cs => 20240411110006_Initialization.cs} | 4 +- .../ApplicationDbContextModelSnapshot.cs | 6 +- .../Repositories/UserRepository.cs | 2 +- .../WeatherSubscriptionRepository.cs | 4 +- .../DatabaseApp.WebApi.csproj | 2 +- .../DatabaseApp.WebApi/Program.cs | 7 +- .../appsettings.Development.json | 12 +- .../DatabaseApp.WebApi/appsettings.json | 37 ++- .../IntegrationWebAppFactory.cs | 32 ++- .../DatabaseApp.Tests.csproj | 7 +- .../DependencyInjection.cs | 30 +++ .../Factories/Common/Metadata.cs | 12 + .../Factories/TelegramCommandFactory.cs | 92 +++++-- .../TelegramCommandValidatorFactory.cs | 25 ++ .../Services/ResendMessageService.cs | 50 ++++ .../TelegramBotApp.Application.csproj | 9 +- .../TelegramBotContext/TelegramBot.cs | 78 ++++-- .../TelegramBotInitializer.cs | 10 +- .../TelegramBotContext/TelegramBotSettings.cs | 14 + .../TelegramCommands/Interfaces.cs | 25 +- .../TelegramCommandValidators.cs | 32 +++ .../TelegramCommands/TelegramCommands.cs | 239 +++++++++++++++++- .../Models/Interfaces.cs | 22 +- .../TelegramBotApp.Domain.csproj | 22 +- .../Caching/CacheService.cs | 1 - .../DependencyInjection.cs | 8 +- .../TelegramBotApp.Caching.csproj | 1 + .../Common/UserSubcriptionInfo.cs | 8 + ... => CompositionPolymorphicTypeResolver.cs} | 0 .../Connection/PersistentConnection.cs | 29 ++- .../DependencyInjection.cs | 6 +- .../EventBusContext/EventBus.cs | 95 +++---- .../EventBusExtensions.cs | 5 +- .../IntegrationContext/Interfaces.cs | 2 +- ...equestUserSubscriptionsIntegrationEvent.cs | 6 + .../CreatedUserIntegrationEvent.cs | 6 +- .../GetAllUsersRequestIntegrationEvent.cs | 6 - ...20\241acheRequestUsersIntegrationEvent.cs" | 6 + ...CreatedUserSubscriptionIntegrationEvent.cs | 9 + ...DeletedUserSubscriptionIntegrationEvent.cs | 8 + ...UpdatedUserSubscriptionIntegrationEvent.cs | 9 + ...WeatherForecastRequestIntegrationEvent.cs} | 0 .../AllUserResponseHandler.cs | 16 -- .../UniversalResponseHandler.cs | 2 +- .../IntegrationResponses/AllUsersResponse.cs | 6 - .../Settings/RabbitMqSettings.cs | 26 +- .../TelegramBotApp.Messaging.csproj | 2 +- .../AppPipeline/AppPipeline.cs | 47 +++- .../TelegramBotApp.Api/appsettings.json | 9 +- .../WeatherDescriptorFormatter.cs | 1 + .../Models/WeatherDescriptor.cs | 3 + ...tWeatherForecastIntegrationEventHandler.cs | 9 +- .../appsettings.Development.json | 11 +- .../WeatherApp.WebApi/appsettings.json | 11 +- .../WeatherApp.Tests/WeatherServiceTests.cs | 4 +- docker-compose.yml | 1 + 75 files changed, 1173 insertions(+), 253 deletions(-) create mode 100644 WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserIntegrationEventHandlers/CreatedUserIntegrationEventHandler.cs create mode 100644 WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/CacheRequestUserSubscriptionsIntegrationEventHandler.cs create mode 100644 WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/CreatedUserSubscriptionIntegrationEventHandler.cs create mode 100644 WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/DeletedUserSubscriptionIntegrationEventHandler.cs create mode 100644 WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/UpdatedUserSubscriptionIntegrationEventHandler.cs rename WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/GetAllUsersRequestIntegrationEventHandler.cs => "WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/\320\241acheRequestUsersIntegrationEventHandler.cs" (51%) rename WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/{20240324163628_Initialize.Designer.cs => 20240411110006_Initialization.Designer.cs} (95%) rename WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/{20240324163628_Initialize.cs => 20240411110006_Initialization.cs} (95%) create mode 100644 WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/DependencyInjection.cs create mode 100644 WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/Common/Metadata.cs create mode 100644 WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/TelegramCommandValidatorFactory.cs create mode 100644 WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Services/ResendMessageService.cs create mode 100644 WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBotSettings.cs create mode 100644 WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/TelegramCommandValidators.cs create mode 100644 WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Common/UserSubcriptionInfo.cs rename WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/{UniversalPolymorphicTypeResolver.cs => CompositionPolymorphicTypeResolver.cs} (100%) create mode 100644 WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/CacheRequestUserSubscriptionsIntegrationEvent.cs delete mode 100644 WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/GetAllUsersRequestIntegrationEvent.cs create mode 100644 "WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/\320\241acheRequestUsersIntegrationEvent.cs" create mode 100644 WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/CreatedUserSubscriptionIntegrationEvent.cs create mode 100644 WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/DeletedUserSubscriptionIntegrationEvent.cs create mode 100644 WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/UpdatedUserSubscriptionIntegrationEvent.cs rename WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/WeatherForecastIntegrationEvents/{RequestWeatherForecastIntegrationEvent.cs => WeatherForecastRequestIntegrationEvent.cs} (100%) delete mode 100644 WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponseHandlers/AllUserResponseHandler.cs delete mode 100644 WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponses/AllUsersResponse.cs diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Common/Mapping/RegisterMapper.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Common/Mapping/RegisterMapper.cs index b865797..29455dc 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Common/Mapping/RegisterMapper.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Common/Mapping/RegisterMapper.cs @@ -10,7 +10,9 @@ public class RegisterMapper : IRegister public void Register(TypeAdapterConfig config) { config.NewConfig() - .Map(dest => dest.Location, src => src.Location.Value); + .Map(dest => dest.Location, src => src.Location.Value) + .Map(dest => dest.ResendInterval, src => src.ResendInterval) + .Map(dest => dest.UserTelegramId, src => src.User.TelegramId); config.NewConfig() .Map(dest => dest.TelegramId, src => src.TelegramId) .Map(dest => dest.MobileNumber, src => src.Metadata.MobileNumber) diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/CreateUserWeatherSubscription/CreateUserWeatherSubscriptionCommand.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/CreateUserWeatherSubscription/CreateUserWeatherSubscriptionCommand.cs index 6606ae6..66f5b11 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/CreateUserWeatherSubscription/CreateUserWeatherSubscriptionCommand.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/CreateUserWeatherSubscription/CreateUserWeatherSubscriptionCommand.cs @@ -5,7 +5,7 @@ namespace DatabaseApp.Application.UserWeatherSubscriptions.Commands.CreateUserWe public class CreateUserWeatherSubscriptionCommand : IRequest { - public int TelegramUserId { get; set; } - public required string Location { get; set; } - public TimeSpan ResendInterval { get; set; } + public long TelegramUserId { get; init; } + public required string Location { get; init; } + public TimeSpan ResendInterval { get; init; } } \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/DeleteUserWeatherSubscription/DeleteUserWeatherSubscriptionCommand.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/DeleteUserWeatherSubscription/DeleteUserWeatherSubscriptionCommand.cs index 293dc63..bc6c8fb 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/DeleteUserWeatherSubscription/DeleteUserWeatherSubscriptionCommand.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/DeleteUserWeatherSubscription/DeleteUserWeatherSubscriptionCommand.cs @@ -5,6 +5,6 @@ namespace DatabaseApp.Application.UserWeatherSubscriptions.Commands.DeleteUserWe public class DeleteUserWeatherSubscriptionCommand : IRequest { - public int UserTelegramId { get; init; } + public long UserTelegramId { get; init; } public required string Location { get; init; } } \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/UpdateUserWeatherSubscription/UpdateUserWeatherSubscriptionCommand.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/UpdateUserWeatherSubscription/UpdateUserWeatherSubscriptionCommand.cs index 9db9b56..56a7892 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/UpdateUserWeatherSubscription/UpdateUserWeatherSubscriptionCommand.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Commands/UpdateUserWeatherSubscription/UpdateUserWeatherSubscriptionCommand.cs @@ -5,7 +5,7 @@ namespace DatabaseApp.Application.UserWeatherSubscriptions.Commands.UpdateUserWe public class UpdateUserWeatherSubscriptionCommand : IRequest { - public int UserTelegramId { get; init; } + public long UserTelegramId { get; init; } public required string Location { get; init; } public TimeSpan ResendInterval { get; init; } } \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Queries/GetWeatherSubscriptions/GetUserWeatherSubscriptionsQuery.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Queries/GetWeatherSubscriptions/GetUserWeatherSubscriptionsQuery.cs index 082641a..edf77a1 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Queries/GetWeatherSubscriptions/GetUserWeatherSubscriptionsQuery.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/Queries/GetWeatherSubscriptions/GetUserWeatherSubscriptionsQuery.cs @@ -4,5 +4,5 @@ namespace DatabaseApp.Application.UserWeatherSubscriptions.Queries.GetWeatherSub public class GetUserWeatherSubscriptionsQuery : IRequest> { - public int UserTelegramId { get; init; } + public long UserTelegramId { get; init; } } \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/UserWeatherSubscriptionDto.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/UserWeatherSubscriptionDto.cs index fbbb26b..38f69c2 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/UserWeatherSubscriptionDto.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/UserWeatherSubscriptions/UserWeatherSubscriptionDto.cs @@ -5,4 +5,6 @@ public class UserWeatherSubscriptionDto { // ReSharper disable once UnusedAutoPropertyAccessor.Global public required string Location { get; set; } + public int UserTelegramId { get; set; } + public TimeSpan ResendInterval { get; set; } } \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/Commands/CreateUser/CreateUserCommand.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/Commands/CreateUser/CreateUserCommand.cs index a22889f..4bfc0ef 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/Commands/CreateUser/CreateUserCommand.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/Commands/CreateUser/CreateUserCommand.cs @@ -3,9 +3,9 @@ namespace DatabaseApp.Application.Users.Commands.CreateUser; -public class CreateUserCommand : IRequest> +public class CreateUserCommand : IRequest> { - public required int TelegramId { get; init; } + public required long TelegramId { get; init; } public required string Username { get; init; } public required string MobileNumber { get; init; } public required DateTime RegisteredAt { get; init; } diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs index 32b1773..962c629 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs @@ -6,15 +6,15 @@ namespace DatabaseApp.Application.Users.Commands.CreateUser; // ReSharper disable once UnusedType.Global -public class CreateUserCommandHandler(IUserRepository repository) : IRequestHandler> +public class CreateUserCommandHandler(IUserRepository repository) : IRequestHandler> { - public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken) + public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken) { var existingUser = await repository.GetByTelegramIdAsync(request.TelegramId, cancellationToken); if (existingUser != null) { - return Result.Fail("User already exists"); + return Result.Fail("User already exists"); } var userMetadata = UserMetadata.Create(request.Username, request.MobileNumber); diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/UserDto.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/UserDto.cs index 918702d..3ec8c9c 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/UserDto.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/Users/UserDto.cs @@ -3,7 +3,7 @@ // ReSharper disable once ClassNeverInstantiated.Global public class UserDto { - public required int TelegramId { get; set; } + public required long TelegramId { get; set; } public required string Username { get; set; } public required string MobileNumber { get; set; } public DateTime RegisteredAt { get; set; } diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/Models/User.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/Models/User.cs index 1ab464e..c968f5e 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/Models/User.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/Models/User.cs @@ -3,7 +3,7 @@ public class User : IEntity { public int Id { get; init; } - public int TelegramId { get; init; } + public long TelegramId { get; init; } public required UserMetadata Metadata { get; init; } public DateTime RegisteredAt { get; init; } } \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/Repositories/Interfaces.cs b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/Repositories/Interfaces.cs index e20c3e4..fa31c08 100644 --- a/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/Repositories/Interfaces.cs +++ b/WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/Repositories/Interfaces.cs @@ -17,16 +17,16 @@ public interface IRepository public interface IUserRepository : IRepository { - Task GetByTelegramIdAsync(int telegramId, CancellationToken cancellationToken); + Task GetByTelegramIdAsync(long telegramId, CancellationToken cancellationToken); Task AddAsync(User user, CancellationToken cancellationToken); Task> GetAllAsync(CancellationToken cancellationToken); } public interface IWeatherSubscriptionRepository : IRepository { - Task> GetAllByUserTelegramId(int userTelegramId, CancellationToken cancellationToken); + Task> GetAllByUserTelegramId(long userTelegramId, CancellationToken cancellationToken); - Task GetByUserTelegramIdAndLocationAsync(int userTelegramId, Location location, + Task GetByUserTelegramIdAndLocationAsync(long userTelegramId, Location location, CancellationToken cancellationToken); Task AddAsync(UserWeatherSubscription weatherSubscription, CancellationToken cancellationToken); diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/EventBusExtensions.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/EventBusExtensions.cs index af0ad8e..02da6db 100644 --- a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/EventBusExtensions.cs +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/EventBusExtensions.cs @@ -1,8 +1,11 @@ using DatabaseApp.IntegrationEvents.IntegrationEventHandlers; +using DatabaseApp.IntegrationEvents.IntegrationEventHandlers.UserIntegrationEventHandlers; +using DatabaseApp.IntegrationEvents.IntegrationEventHandlers.UserSubscriptionEventHandlers; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using TelegramBotApp.Messaging; using TelegramBotApp.Messaging.IntegrationContext.UserIntegrationEvents; +using TelegramBotApp.Messaging.IntegrationContext.UserSubscriptionEvents; namespace DatabaseApp.IntegrationEvents; @@ -12,8 +15,13 @@ public static class EventBusExtensions public static IApplicationBuilder SubscribeToEvents(this IApplicationBuilder app) { var eventBus = app.ApplicationServices.GetRequiredService(); - - eventBus.Subscribe(); + + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); return app; } diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserIntegrationEventHandlers/CreatedUserIntegrationEventHandler.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserIntegrationEventHandlers/CreatedUserIntegrationEventHandler.cs new file mode 100644 index 0000000..5365b52 --- /dev/null +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserIntegrationEventHandlers/CreatedUserIntegrationEventHandler.cs @@ -0,0 +1,38 @@ +using DatabaseApp.Application.Users.Commands.CreateUser; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using TelegramBotApp.Caching.Caching; +using TelegramBotApp.Messaging.IntegrationContext; +using TelegramBotApp.Messaging.IntegrationContext.UserIntegrationEvents; +using TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; + +namespace DatabaseApp.IntegrationEvents.IntegrationEventHandlers.UserIntegrationEventHandlers; + +public class CreatedUserIntegrationEventHandler(IServiceScopeFactory factory, ICacheService cacheService) + : IIntegrationEventHandler +{ + public async Task Handle(CreatedUserIntegrationEvent @event) + { + using var scope = factory.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + var result = await mediator.Send(new CreateUserCommand + { + TelegramId = @event.UserTelegramId, // TODO + Username = @event.Username, + MobileNumber = @event.MobileNumber, + RegisteredAt = @event.RegisteredAt + }); + + if (!result.IsSuccess) return UniversalResponse.Empty; + + var allUsers = await cacheService.GetAsync>("allUsers"); + await cacheService.RemoveAsync("allUsers"); + + allUsers ??= []; + allUsers.Add(@event.UserTelegramId); + + await cacheService.SetAsync("allUsers", allUsers); + + return UniversalResponse.Empty; + } +} \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/CacheRequestUserSubscriptionsIntegrationEventHandler.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/CacheRequestUserSubscriptionsIntegrationEventHandler.cs new file mode 100644 index 0000000..77fb79b --- /dev/null +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/CacheRequestUserSubscriptionsIntegrationEventHandler.cs @@ -0,0 +1,54 @@ +using DatabaseApp.Application.Users.Queries.GetAllUsers; +using DatabaseApp.Application.UserWeatherSubscriptions.Queries.GetWeatherSubscriptions; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using TelegramBotApp.Caching.Caching; +using TelegramBotApp.Messaging.Common; +using TelegramBotApp.Messaging.IntegrationContext; +using TelegramBotApp.Messaging.IntegrationContext.UserIntegrationEvents; +using TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; + +namespace DatabaseApp.IntegrationEvents.IntegrationEventHandlers.UserSubscriptionEventHandlers; + +public class CacheRequestUserSubscriptionsIntegrationEventHandler( + IServiceScopeFactory factory, + ICacheService cacheService) + : IIntegrationEventHandler +{ + public async Task Handle(CacheRequestUserSubscriptionsIntegrationEvent @event) + { + using var scope = factory.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + List subscriptionInfos = []; + var userTelegramIds = await cacheService.GetAsync>("allUsers"); + + if (userTelegramIds is null) + { + var userDtos = await mediator.Send(new GetAllUsersQuery()); + userTelegramIds = userDtos.Select(dto => dto.TelegramId).ToList(); + await cacheService.SetAsync("allUsers", userTelegramIds); + } + + foreach (var telegramId in userTelegramIds) + { + var userSubscription = await mediator.Send(new GetUserWeatherSubscriptionsQuery + { + UserTelegramId = telegramId + }); + + if (userSubscription.Count == 0) continue; + + subscriptionInfos.AddRange(userSubscription.Select(subscriptionDto => new UserSubscriptionInfo + { + TelegramId = subscriptionDto.UserTelegramId, + ResendInterval = subscriptionDto.ResendInterval, + Location = subscriptionDto.Location + })); + } + + await cacheService.RemoveAsync("allSubscriptions"); + await cacheService.SetAsync("allSubscriptions", subscriptionInfos); + + return UniversalResponse.Empty; + } +} \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/CreatedUserSubscriptionIntegrationEventHandler.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/CreatedUserSubscriptionIntegrationEventHandler.cs new file mode 100644 index 0000000..a018977 --- /dev/null +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/CreatedUserSubscriptionIntegrationEventHandler.cs @@ -0,0 +1,44 @@ +using DatabaseApp.Application.UserWeatherSubscriptions.Commands.CreateUserWeatherSubscription; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using TelegramBotApp.Caching.Caching; +using TelegramBotApp.Messaging.Common; +using TelegramBotApp.Messaging.IntegrationContext; +using TelegramBotApp.Messaging.IntegrationContext.UserSubscriptionEvents; +using TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; + +namespace DatabaseApp.IntegrationEvents.IntegrationEventHandlers.UserSubscriptionEventHandlers; + +public class CreatedUserSubscriptionIntegrationEventHandler(IServiceScopeFactory factory, ICacheService cacheService) + : IIntegrationEventHandler +{ + public async Task Handle(CreatedUserSubscriptionIntegrationEvent @event) + { + using var scope = factory.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + var result = await mediator.Send(new CreateUserWeatherSubscriptionCommand + { + Location = @event.City, + ResendInterval = @event.ResendInterval, + TelegramUserId = @event.TelegramId + }); + + if (result.IsFailed) return new UniversalResponse(result.Errors.First().Message); + + var allSubscriptions = await cacheService.GetAsync>("allSubscriptions"); + await cacheService.RemoveAsync("allSubscriptions"); + + allSubscriptions ??= []; + + allSubscriptions.Add(new() + { + ResendInterval = @event.ResendInterval, + Location = @event.City, + TelegramId = @event.TelegramId + }); + await cacheService.SetAsync("allSubscriptions", allSubscriptions); + + return new UniversalResponse("Subscription created successfully"); + } +} \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/DeletedUserSubscriptionIntegrationEventHandler.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/DeletedUserSubscriptionIntegrationEventHandler.cs new file mode 100644 index 0000000..d4ade5d --- /dev/null +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/DeletedUserSubscriptionIntegrationEventHandler.cs @@ -0,0 +1,40 @@ +using DatabaseApp.Application.UserWeatherSubscriptions.Commands.DeleteUserWeatherSubscription; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using TelegramBotApp.Caching.Caching; +using TelegramBotApp.Messaging.Common; +using TelegramBotApp.Messaging.IntegrationContext; +using TelegramBotApp.Messaging.IntegrationContext.UserSubscriptionEvents; +using TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; + +namespace DatabaseApp.IntegrationEvents.IntegrationEventHandlers.UserSubscriptionEventHandlers; + +public class DeletedUserSubscriptionIntegrationEventHandler(IServiceScopeFactory factory, ICacheService cacheService) + : IIntegrationEventHandler +{ + public async Task Handle(DeletedUserSubscriptionIntegrationEvent @event) + { + using var scope = factory.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + var result = await mediator.Send(new DeleteUserWeatherSubscriptionCommand + { + UserTelegramId = @event.TelegramUserId, + Location = @event.Location + }); + + if (result.IsFailed) return new UniversalResponse(result.Errors.First().Message); + + var allSubscriptions = await cacheService.GetAsync>("allSubscriptions"); + await cacheService.RemoveAsync("allSubscriptions"); + + allSubscriptions ??= []; + + allSubscriptions.RemoveAll(x => + x.TelegramId == @event.TelegramUserId && x.Location == @event.Location); + + await cacheService.SetAsync("allSubscriptions", allSubscriptions); + + return new UniversalResponse("Subscription deleted successfully"); + } +} \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/UpdatedUserSubscriptionIntegrationEventHandler.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/UpdatedUserSubscriptionIntegrationEventHandler.cs new file mode 100644 index 0000000..cb039c4 --- /dev/null +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/UpdatedUserSubscriptionIntegrationEventHandler.cs @@ -0,0 +1,47 @@ +using DatabaseApp.Application.UserWeatherSubscriptions.Commands.UpdateUserWeatherSubscription; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using TelegramBotApp.Caching.Caching; +using TelegramBotApp.Messaging.Common; +using TelegramBotApp.Messaging.IntegrationContext; +using TelegramBotApp.Messaging.IntegrationContext.UserSubscriptionEvents; +using TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; + +namespace DatabaseApp.IntegrationEvents.IntegrationEventHandlers.UserSubscriptionEventHandlers; + +public class UpdatedUserSubscriptionIntegrationEventHandler(IServiceScopeFactory factory, ICacheService cacheService) + : IIntegrationEventHandler +{ + public async Task Handle(UpdatedUserSubscriptionIntegrationEvent @event) + { + using var scope = factory.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + var result = await mediator.Send(new UpdateUserWeatherSubscriptionCommand + { + Location = @event.Location, + ResendInterval = @event.ResendInterval, + UserTelegramId = @event.TelegramUserId + }); + + if (result.IsFailed) return new UniversalResponse(result.Errors.First().Message); + + var allSubscriptions = await cacheService.GetAsync>("allSubscriptions"); + await cacheService.RemoveAsync("allSubscriptions"); + + allSubscriptions ??= []; + + allSubscriptions.RemoveAll(x => + x.TelegramId == @event.TelegramUserId && x.Location == @event.Location); + + allSubscriptions.Add(new() + { + ResendInterval = @event.ResendInterval, + Location = @event.Location, + TelegramId = @event.TelegramUserId + }); + await cacheService.SetAsync("allSubscriptions", allSubscriptions); + + return new UniversalResponse("Subscription updated successfully"); + } +} \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/GetAllUsersRequestIntegrationEventHandler.cs "b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/\320\241acheRequestUsersIntegrationEventHandler.cs" similarity index 51% rename from WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/GetAllUsersRequestIntegrationEventHandler.cs rename to "WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/\320\241acheRequestUsersIntegrationEventHandler.cs" index 9b941f6..585fdb2 100644 --- a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/GetAllUsersRequestIntegrationEventHandler.cs +++ "b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/IntegrationEventHandlers/UserSubscriptionEventHandlers/\320\241acheRequestUsersIntegrationEventHandler.cs" @@ -1,20 +1,27 @@ using DatabaseApp.Application.Users.Queries.GetAllUsers; using MediatR; using Microsoft.Extensions.DependencyInjection; +using TelegramBotApp.Caching.Caching; using TelegramBotApp.Messaging.IntegrationContext; using TelegramBotApp.Messaging.IntegrationContext.UserIntegrationEvents; using TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; -namespace DatabaseApp.IntegrationEvents.IntegrationEventHandlers; +namespace DatabaseApp.IntegrationEvents.IntegrationEventHandlers.UserSubscriptionEventHandlers; -public class GetAllUsersRequestIntegrationEventHandler(IServiceScopeFactory factory) - : IIntegrationEventHandler +public class CacheRequestUsersIntegrationEventHandler(IServiceScopeFactory factory, ICacheService cacheService) + : IIntegrationEventHandler { - public async Task Handle(GetAllUsersRequestIntegrationEvent @event) + public async Task Handle(CacheRequestUsersIntegrationEvent @event) { using var scope = factory.CreateScope(); var mediator = scope.ServiceProvider.GetRequiredService(); + var users = await mediator.Send(new GetAllUsersQuery()); - return new AllUsersResponse { UserTelegramIds = users.Select(u => u.TelegramId).ToList() }; + var telegramIds = users.Select(u => u.TelegramId).ToList(); + + await cacheService.RemoveAsync("allUsers"); + await cacheService.SetAsync("allUsers", telegramIds); + + return UniversalResponse.Empty; } } \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240324163628_Initialize.Designer.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240411110006_Initialization.Designer.cs similarity index 95% rename from WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240324163628_Initialize.Designer.cs rename to WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240411110006_Initialization.Designer.cs index bbde4c3..63e730c 100644 --- a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240324163628_Initialize.Designer.cs +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240411110006_Initialization.Designer.cs @@ -12,8 +12,8 @@ namespace DatabaseApp.Persistence.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20240324163628_Initialize")] - partial class Initialize + [Migration("20240411110006_Initialization")] + partial class Initialization { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -39,8 +39,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("TIMESTAMP"); - b.Property("TelegramId") - .HasColumnType("integer") + b.Property("TelegramId") + .HasColumnType("bigint") .HasColumnName("TELEGRAM_ID"); b.HasKey("Id"); @@ -80,7 +80,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b1.Property("UserId") .HasColumnType("integer"); - b1.Property("Number") + b1.Property("MobileNumber") .IsRequired() .HasColumnType("text") .HasColumnName("MOBILE_NUMBER"); diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240324163628_Initialize.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240411110006_Initialization.cs similarity index 95% rename from WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240324163628_Initialize.cs rename to WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240411110006_Initialization.cs index 830fc99..da4df4e 100644 --- a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240324163628_Initialize.cs +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/20240411110006_Initialization.cs @@ -7,7 +7,7 @@ namespace DatabaseApp.Persistence.Migrations { /// - public partial class Initialize : Migration + public partial class Initialization : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -19,7 +19,7 @@ protected override void Up(MigrationBuilder migrationBuilder) ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:IdentitySequenceOptions", "'1', '1', '', '', 'False', '1'") .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - TELEGRAM_ID = table.Column(type: "integer", nullable: false), + TELEGRAM_ID = table.Column(type: "bigint", nullable: false), USERNAME = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), MOBILE_NUMBER = table.Column(type: "text", nullable: false), TIMESTAMP = table.Column(type: "timestamp with time zone", nullable: false) diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 863b8f5..60b442a 100644 --- a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -36,8 +36,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("TIMESTAMP"); - b.Property("TelegramId") - .HasColumnType("integer") + b.Property("TelegramId") + .HasColumnType("bigint") .HasColumnName("TELEGRAM_ID"); b.HasKey("Id"); @@ -77,7 +77,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.Property("UserId") .HasColumnType("integer"); - b1.Property("Number") + b1.Property("MobileNumber") .IsRequired() .HasColumnType("text") .HasColumnName("MOBILE_NUMBER"); diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Repositories/UserRepository.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Repositories/UserRepository.cs index f8175cc..82dd648 100644 --- a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Repositories/UserRepository.cs +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Repositories/UserRepository.cs @@ -8,7 +8,7 @@ namespace DatabaseApp.Persistence.Repositories; public class UserRepository(IDatabaseContext context) : RepositoryBase(context), IUserRepository { - public Task GetByTelegramIdAsync(int telegramId, CancellationToken cancellationToken) => + public Task GetByTelegramIdAsync(long telegramId, CancellationToken cancellationToken) => _context.Users .FirstOrDefaultAsync(u => u.TelegramId == telegramId, cancellationToken); diff --git a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Repositories/WeatherSubscriptionRepository.cs b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Repositories/WeatherSubscriptionRepository.cs index 1a21566..8db8496 100644 --- a/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Repositories/WeatherSubscriptionRepository.cs +++ b/WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/Repositories/WeatherSubscriptionRepository.cs @@ -8,14 +8,14 @@ namespace DatabaseApp.Persistence.Repositories; public class WeatherSubscriptionRepository(IDatabaseContext context) : RepositoryBase(context), IWeatherSubscriptionRepository { - public Task> GetAllByUserTelegramId(int userTelegramId, + public Task> GetAllByUserTelegramId(long userTelegramId, CancellationToken cancellationToken) => _context.UserWeatherSubscriptions .Include(s => s.User) .Where(s => s.User.TelegramId == userTelegramId) .ToListAsync(cancellationToken); - public Task GetByUserTelegramIdAndLocationAsync(int userTelegramId, Location location, + public Task GetByUserTelegramIdAndLocationAsync(long userTelegramId, Location location, CancellationToken cancellationToken) => _context.UserWeatherSubscriptions .Include(s => s.User) diff --git a/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/DatabaseApp.WebApi.csproj b/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/DatabaseApp.WebApi.csproj index dc74c2a..95ca744 100644 --- a/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/DatabaseApp.WebApi.csproj +++ b/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/DatabaseApp.WebApi.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/Program.cs b/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/Program.cs index e1c78d3..19821a3 100644 --- a/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/Program.cs +++ b/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/Program.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Any; using Serilog; +using TelegramBotApp.Caching; using TelegramBotApp.Messaging; var builder = WebApplication.CreateBuilder(args); @@ -30,7 +31,11 @@ builder.Services.AddExceptionHandler(); builder.Services.AddProblemDetails(); builder.Services.AddControllers(); -builder.Services.AddApplication().AddPersistence(builder.Configuration).AddMessaging(builder.Configuration); +builder.Services + .AddApplication() + .AddPersistence(builder.Configuration) + .AddMessaging(builder.Configuration) + .AddCaching(builder.Configuration); var app = builder.Build(); diff --git a/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/appsettings.Development.json b/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/appsettings.Development.json index 6e71929..f0b1fe1 100644 --- a/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/appsettings.Development.json +++ b/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/appsettings.Development.json @@ -1,6 +1,7 @@ { "ConnectionStrings": { - "DbConnection": "Host=localhost:5755; Database=weather-database; Username=user; Password=pass" + "DbConnection": "Host=localhost:5755; Database=weather-database; Username=user; Password=pass", + "Redis": "localhost:6379" }, "Serilog": { "Using": [ @@ -19,6 +20,13 @@ ] }, "RabbitMqSettings": { - "HostName": "localhost" + "HostName": "localhost", + "Port": 15672, + "Username": "guest", + "Password": "guest", + "EventQueueName": "database-event-queue", + "ResponseQueueName": "database-response-queue", + "EventExchangeName": "event-exchange", + "ResponseExchangeName": "response-exchange" } } diff --git a/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/appsettings.json b/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/appsettings.json index 10f68b8..acf97a7 100644 --- a/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/appsettings.json +++ b/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/appsettings.json @@ -1,9 +1,32 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } + "ConnectionStrings": { + "DbConnection": "Host=localhost:5755; Database=weather-database; Username=user; Password=pass", + "Redis": "localhost:6379" }, - "AllowedHosts": "*" -} + "Serilog": { + "Using": [ + "Serilog.Sinks.Console" + ], + "MinimumLevel": "Debug", + "WriteTo": [ + { + "Name": "Console" + } + ], + "Enrich": [ + "FromLogContext", + "WithMachineName", + "WithThreadId" + ] + }, + "RabbitMqSettings": { + "HostName": "localhost", + "Port": 15672, + "Username": "guest", + "Password": "guest", + "EventQueueName": "database-event-queue", + "ResponseQueueName": "database-response-queue", + "EventExchangeName": "event-exchange", + "ResponseExchangeName": "response-exchange" + } +} \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/BasicTestContext/IntegrationWebAppFactory.cs b/WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/BasicTestContext/IntegrationWebAppFactory.cs index 47eb3aa..34ed0d4 100644 --- a/WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/BasicTestContext/IntegrationWebAppFactory.cs +++ b/WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/BasicTestContext/IntegrationWebAppFactory.cs @@ -6,7 +6,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Respawn; +using TelegramBotApp.Messaging; +using TelegramBotApp.Messaging.Settings; using Testcontainers.PostgreSql; +using Testcontainers.RabbitMq; using Xunit; namespace DatabaseApp.Tests.BasicTestContext; @@ -20,6 +23,12 @@ public class IntegrationWebAppFactory : WebApplicationFactory, IAsyncLi .WithUsername("user") .WithPassword("pass").Build(); + private readonly RabbitMqContainer _rabbitMqContainer = new RabbitMqBuilder() + .WithImage("rabbitmq:latest") + .WithUsername("guest") + .WithPassword("guest") + .Build(); + private Respawner _respawner = null!; private DbConnection _connection = null!; @@ -37,6 +46,23 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.AddDbContext(options => options.UseNpgsql(_dbContainer.GetConnectionString())); + + descriptor = services.FirstOrDefault(d => + d.ServiceType == typeof(IMessageSettings)); + + if (descriptor != null) + { + services.Remove(descriptor); + } + + services.AddSingleton(new RabbitMqSettings + { + EventExchangeName = "event-exchange", + ResponseExchangeName = "response-exchange", + EventQueueName = "event-queue", + ResponseQueueName = "response-queue", + ConnectionString = _rabbitMqContainer.GetConnectionString() + }); }); } @@ -45,6 +71,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) public async Task InitializeAsync() { await _dbContainer.StartAsync(); + await _rabbitMqContainer.StartAsync(); _connection = Services.CreateScope().ServiceProvider.GetRequiredService() .Db.GetDbConnection(); await _connection.OpenAsync(); @@ -55,5 +82,8 @@ public async Task InitializeAsync() }); } - public new Task DisposeAsync() => _dbContainer.DisposeAsync().AsTask(); + public new async Task DisposeAsync() => + await Task.WhenAll( + _dbContainer.DisposeAsync().AsTask(), + _rabbitMqContainer.DisposeAsync().AsTask()); } \ No newline at end of file diff --git a/WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/DatabaseApp.Tests.csproj b/WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/DatabaseApp.Tests.csproj index 317961b..1887dfa 100644 --- a/WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/DatabaseApp.Tests.csproj +++ b/WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/DatabaseApp.Tests.csproj @@ -11,7 +11,7 @@ - + @@ -20,12 +20,13 @@ + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/DependencyInjection.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/DependencyInjection.cs new file mode 100644 index 0000000..7dedcf8 --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/DependencyInjection.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TelegramBotApp.Application.Factories; +using TelegramBotApp.Application.Services; +using TelegramBotApp.Application.TelegramBotContext; +using TelegramBotApp.Domain.Models; + +namespace TelegramBotApp.Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(s => + { + var botInitializer = s.GetRequiredService(); + return botInitializer.CreateBot(configuration.GetSection("TelegramSettings:BotToken").Value ?? + throw new InvalidOperationException("Bot token is not set."), + botInitializer.CreateReceiverOptions()); + }); + + return services; + } +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/Common/Metadata.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/Common/Metadata.cs new file mode 100644 index 0000000..3ead27e --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/Common/Metadata.cs @@ -0,0 +1,12 @@ +namespace TelegramBotApp.Application.Factories.Common; + +// ReSharper disable once ClassNeverInstantiated.Global +internal class TelegramCommandMetadata +{ + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global + public string Command { get; set; } = string.Empty; + + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global + public string Description { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/TelegramCommandFactory.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/TelegramCommandFactory.cs index 3a3b1c5..1ce48b3 100644 --- a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/TelegramCommandFactory.cs +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/TelegramCommandFactory.cs @@ -1,35 +1,95 @@ +using System.Collections.Concurrent; +using System.CommandLine; using System.Composition; using FluentResults; +using TelegramBotApp.Application.Factories.Common; +using TelegramBotApp.Application.Services; using TelegramBotApp.Application.TelegramCommands; +using TelegramBotApp.Caching.Caching; +using TelegramBotApp.Domain.Models; +using TelegramBotApp.Messaging; namespace TelegramBotApp.Application.Factories; -public static class TelegramCommandFactory +public class TelegramCommandFactory { - // ReSharper disable once ClassNeverInstantiated.Local - private class TelegramCommandMetadata - { - // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local - public string Command { get; set; } = string.Empty; - } - private class ImportInfo { [ImportMany] // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local - public IEnumerable> Commands { get; set; } = - Enumerable.Empty>(); + public ExportFactory[] Factories { get; set; } = []; } private static readonly ImportInfo s_info = new(); + private readonly RootCommand _rootCommand = new("Root command"); + private readonly Option _globalOption; + private readonly ConcurrentDictionary> _results = new(); + private readonly IEventBus _bus; + private readonly ICacheService _cacheService; + private readonly ITelegramBotSettings _settings; + private readonly IResendMessageService _messageService; - static TelegramCommandFactory() => MefContainerConfiguration.SatisfyImports(s_info); + public TelegramCommandFactory(IEventBus bus, ICacheService cacheService, IResendMessageService messageService, + ITelegramBotSettings settings) + { + MefContainerConfiguration.SatisfyImports(s_info); + _bus = bus; + _settings = settings; + _cacheService = cacheService; + _messageService = messageService; + + _globalOption = new( + name: "--telegram-id", + description: "Telegram chat id"); + + _rootCommand.AddGlobalOption(_globalOption); + + foreach (var exportFactory in s_info.Factories) + { + _rootCommand.AddCommand(InitializeCommand(exportFactory.CreateExport().Value)); + } + } - public static Result GetCommand(string command) + public async Task> StartCommand(string args, long telegramId) { - var commandMetadata = s_info.Commands.FirstOrDefault(x => x.Metadata.Command == command); - return commandMetadata != null - ? Result.Ok(commandMetadata.Value) - : Result.Fail($"Command {command} not found"); + await _rootCommand.InvokeAsync($"{args} --telegram-id {telegramId}"); + _results.TryRemove(telegramId, out var result); + return result ?? Result.Fail("Command not found or bad arguments"); } + + private Command InitializeCommand(ITelegramCommand telegramCommand) + { + var command = new Command(telegramCommand.Command); + var arguments = telegramCommand.Arguments.ToList(); + + foreach (var argument in arguments) + { + // TODO: Uncomment when validators are implemented + // argument.AddValidator(TelegramCommandValidatorFactory.GetValidatorForCommand(telegramCommand.Command).GetValidator()); + command.AddArgument(argument); + } + + command.SetHandler(async context => + { + var telegramId = context.ParseResult.GetValueForOption(_globalOption); + var result = await telegramCommand.Execute( + context, + telegramId, + GetValueForArgument, + _bus, + _cacheService, + _messageService, + _settings.Token); + _results.TryAdd(telegramId, result.Value); + }); + + return command; + + Argument GetValueForArgument(string name) => + arguments.FirstOrDefault(a => a.Name == name) ?? + throw new ArgumentException($"Argument not found with name '{name}'", nameof(name)); + } + + public static IEnumerable GetCommands() => + s_info.Factories.Select(f => $"{f.Metadata.Command} {f.Metadata.Description}"); } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/TelegramCommandValidatorFactory.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/TelegramCommandValidatorFactory.cs new file mode 100644 index 0000000..39b9ec1 --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Factories/TelegramCommandValidatorFactory.cs @@ -0,0 +1,25 @@ +using System.Composition; +using TelegramBotApp.Application.Factories.Common; +using TelegramBotApp.Application.TelegramCommands; + +namespace TelegramBotApp.Application.Factories; + +public class TelegramCommandValidatorFactory +{ + private class ImportInfo + { + [ImportMany] + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local + public IEnumerable> Validators { get; set; } = + Enumerable.Empty>(); + } + + private static readonly ImportInfo s_info = new(); + + static TelegramCommandValidatorFactory() => + MefContainerConfiguration.SatisfyImports(s_info); + + public static ITelegramCommandValidator GetValidatorForCommand(string command) => + s_info.Validators.FirstOrDefault(x => x.Metadata.Command == command)?.Value ?? + new DefaultTelegramCommandValidator(); +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Services/ResendMessageService.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Services/ResendMessageService.cs new file mode 100644 index 0000000..9e15756 --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/Services/ResendMessageService.cs @@ -0,0 +1,50 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using TelegramBotApp.Domain.Models; +using TelegramBotApp.Messaging; +using TelegramBotApp.Messaging.IntegrationContext.WeatherForecastIntegrationEvents; +using TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; + +namespace TelegramBotApp.Application.Services; + +[SuppressMessage("ReSharper", "AsyncVoidLambda")] +public class ResendMessageService(ITelegramBot bot, IEventBus bus) : IResendMessageService +{ + private readonly ConcurrentDictionary> _timers = new(); + + public void AddOrUpdateResendProcess(long telegramId, string location, TimeSpan interval) + { + var timer = new Timer(async _ => + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); // TODO: add common settings + var response = await bus.Publish(new WeatherForecastRequestIntegrationEvent(location), + replyTo: nameof(UniversalResponse), cancellationToken: cts.Token); + await bot.SendMessageAsync(telegramId, response.Message); + }, null, interval, interval); + + _timers.AddOrUpdate(telegramId, _ => [new(timer, location)], + (_, timers) => + { + var resendTimer = timers.Find(x => x.Location == location); + + if (resendTimer != null) + { + resendTimer.Timer.Dispose(); + timers.Remove(resendTimer); + } + + timers.Add(new(timer, location)); + return timers; + }); + } + + public void RemoveResendProcess(long telegramId, string location) + { + if (!_timers.TryGetValue(telegramId, out var timers)) return; + var timer = timers.FirstOrDefault(x => x.Location == location); + timer?.Timer.Dispose(); + if (timer != null) timers.Remove(timer); + } +} + +internal record ResendTimer(Timer Timer, string Location); \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotApp.Application.csproj b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotApp.Application.csproj index 6c82e7f..ddc6def 100644 --- a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotApp.Application.csproj +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotApp.Application.csproj @@ -7,14 +7,15 @@ - - + + + - - + + diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBot.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBot.cs index 90de314..e1ec0dd 100644 --- a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBot.cs +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBot.cs @@ -1,25 +1,41 @@ +using FluentResults; using Telegram.Bot; using Telegram.Bot.Exceptions; using Telegram.Bot.Polling; using Telegram.Bot.Types; using TelegramBotApp.Application.Factories; +using TelegramBotApp.Application.Services; +using TelegramBotApp.Caching.Caching; using TelegramBotApp.Domain.Models; using TelegramBotApp.Messaging; +using TelegramBotApp.Messaging.IntegrationContext.UserIntegrationEvents; namespace TelegramBotApp.Application.TelegramBotContext; -public class TelegramBot(ITelegramBotClient telegramBot) : ITelegramBot +public class TelegramBot(ITelegramBotClient telegramBot, ReceiverOptions receiverOptions) : ITelegramBot { - private readonly TimeSpan _timeout = TimeSpan.FromSeconds(30); private const string HelpCommand = "/help"; - public void StartReceiving(ReceiverOptions receiverOptions, IEventBus bus, CancellationToken cancellationToken) => + private readonly ITelegramBotSettings _settings = TelegramBotSettings.CreateDefault(); + private TelegramCommandFactory _telegramCommandFactory = null!; // TODO: Refactor to use DI + private ICacheService _cacheService = null!; + + public void StartReceiving(IEventBus bus, ICacheService cacheService, IResendMessageService messageService, + CancellationToken cancellationToken) + { + _telegramCommandFactory = new(bus, cacheService, messageService, _settings); + _cacheService = cacheService; + telegramBot.StartReceiving( updateHandler: async (botClient, update, token) => await HandleUpdateInnerAsync(botClient, update, bus, token), pollingErrorHandler: HandlePollingErrorInner, receiverOptions: receiverOptions, cancellationToken: cancellationToken); + } + + public Task SendMessageAsync(long telegramId, string message) => + telegramBot.SendTextMessageAsync(telegramId, message); public Task GetMeAsync() => telegramBot.GetMeAsync(); @@ -29,45 +45,39 @@ private async Task HandleUpdateInnerAsync(ITelegramBotClient botClient, Update u if (update.Message is not { } message) return; if (message.Text is not { } messageText) return; - var chatId = message.Chat.Id; - using var cts = new CancellationTokenSource(_timeout); + var chatId = message.Chat.Id; // chat id equals to user telegram id + using var cts = new CancellationTokenSource(_settings.Timeout); try { - var args = messageText.Split(' '); - var command = TelegramCommandFactory.GetCommand(args[0]); - - if (command.IsFailed) - { - await HandleError(botClient, chatId); - return; - } + _ = Task.Run(async () => await UpdateUsersCacheAsync( + message, + bus, + cancellationToken), + cancellationToken); + var result = await _telegramCommandFactory.StartCommand(messageText, chatId); - var response = - await command.Value.Execute(args[0], args.Length > 1 ? args[1] : string.Empty, bus, cts.Token); - - if (response.IsFailed) + if (result.IsFailed) { - await HandleError(botClient, chatId); + await HandleError(botClient, chatId, result); return; } - await botClient.SendTextMessageAsync(chatId: chatId, response.Value, + await botClient.SendTextMessageAsync(chatId: chatId, result.Value, cancellationToken: cancellationToken); } catch (Exception) { - await HandleError(botClient, chatId); + await HandleError(botClient, chatId, result: Result.Fail("Internal error")); } return; - async Task HandleError(ITelegramBotClient bot, long chatIdInner) + async Task HandleError(ITelegramBotClient bot, long chatIdInner, IResultBase result) { - var text = await TelegramCommandFactory - .GetCommand(HelpCommand) - .Value - .Execute(HelpCommand, string.Empty, bus, CancellationToken.None); + await bot.SendTextMessageAsync(chatId: chatIdInner, result.Errors.First().Message, + cancellationToken: cancellationToken); + var text = await _telegramCommandFactory.StartCommand(HelpCommand, chatIdInner); await bot.SendTextMessageAsync(chatId: chatIdInner, text.Value, cancellationToken: cancellationToken); } } @@ -83,7 +93,23 @@ ApiRequestException apiRequestException }; Console.WriteLine(errorMessage); - + return Task.CompletedTask; } + + private async Task UpdateUsersCacheAsync(Message message, IEventBus bus, CancellationToken cancellationToken) + { + var userTelegramIds = await _cacheService.GetAsync>("allUsers", cancellationToken); + + if (userTelegramIds?.Contains(message.From!.Id) == false) + { + _ = await bus.Publish(new CreatedUserIntegrationEvent + { + MobileNumber = message.Contact?.PhoneNumber ?? "fake-number", + Username = message.From?.Username ?? string.Empty, + UserTelegramId = message.From?.Id ?? 0, + RegisteredAt = DateTime.UtcNow + }, cancellationToken: cancellationToken); + } + } } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBotInitializer.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBotInitializer.cs index a20f958..f552c07 100644 --- a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBotInitializer.cs +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBotInitializer.cs @@ -5,14 +5,18 @@ namespace TelegramBotApp.Application.TelegramBotContext; -public class TelegramBotInitializer : IBotInitializer +public class TelegramBotInitializer : ITelegramBotInitializer { - public ITelegramBot CreateBot(string token) +#pragma warning disable CA1822 + public ITelegramBot CreateBot(string token, ReceiverOptions receiverOptions) +#pragma warning restore CA1822 { - return new TelegramBot(new TelegramBotClient(token)); + return new TelegramBot(new TelegramBotClient(token), receiverOptions); } +#pragma warning disable CA1822 public ReceiverOptions CreateReceiverOptions() => +#pragma warning restore CA1822 new() { AllowedUpdates = [UpdateType.Message] diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBotSettings.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBotSettings.cs new file mode 100644 index 0000000..cb7cb18 --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramBotContext/TelegramBotSettings.cs @@ -0,0 +1,14 @@ +using TelegramBotApp.Domain.Models; + +namespace TelegramBotApp.Application.TelegramBotContext; + +public class TelegramBotSettings : ITelegramBotSettings +{ + private readonly CancellationTokenSource _cts; + public TimeSpan Timeout => TimeSpan.FromSeconds(10); + public CancellationToken Token => _cts.Token; + + private TelegramBotSettings() => _cts = new(Timeout); + + public static ITelegramBotSettings CreateDefault() => new TelegramBotSettings(); +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/Interfaces.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/Interfaces.cs index 1f65f01..f1a3413 100644 --- a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/Interfaces.cs +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/Interfaces.cs @@ -1,4 +1,10 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.CommandLine.Parsing; using FluentResults; +using TelegramBotApp.Application.Services; +using TelegramBotApp.Caching.Caching; +using TelegramBotApp.Domain.Models; using TelegramBotApp.Messaging; namespace TelegramBotApp.Application.TelegramCommands; @@ -6,6 +12,21 @@ namespace TelegramBotApp.Application.TelegramCommands; public interface ITelegramCommand { string Command { get; } - - Task> Execute(string command, string value, IEventBus bus, CancellationToken cancellationToken); + string Description { get; } + IEnumerable> Arguments { get; } + + Task> Execute(InvocationContext context, + long telegramId, + Func> getArgument, + IEventBus bus, + ICacheService cacheService, + IResendMessageService messageService, + CancellationToken cancellationToken); +} + +public interface ITelegramCommandValidator +{ + string Command { get; } + + ParseArgument GetValidator(); } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/TelegramCommandValidators.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/TelegramCommandValidators.cs new file mode 100644 index 0000000..dfa0799 --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/TelegramCommandValidators.cs @@ -0,0 +1,32 @@ +using System.CommandLine.Parsing; +using System.Composition; +using FluentResults; + +namespace TelegramBotApp.Application.TelegramCommands; + +public class DefaultTelegramCommandValidator : ITelegramCommandValidator +{ + public string Command => string.Empty; + + public ParseArgument GetValidator() => _ => Result.Ok(); +} + +[Export(typeof(ITelegramCommandValidator))] +[ExportMetadata(nameof(Command), "/help")] +public class HelpTelegramCommandValidator : ITelegramCommandValidator +{ + public string Command => "/help"; + + public ParseArgument GetValidator() => + _ => Result.Ok(); +} + +[Export(typeof(ITelegramCommandValidator))] +[ExportMetadata(nameof(Command), "/weather")] +public class WeatherTelegramCommandValidator : ITelegramCommandValidator +{ + public string Command => "/weather"; + + public ParseArgument GetValidator() => + result => result.Tokens.Count == 0 ? Result.Fail("City name is required") : Result.Ok(); +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/TelegramCommands.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/TelegramCommands.cs index c8e2eb1..71c7273 100644 --- a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/TelegramCommands.cs +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/TelegramCommands/TelegramCommands.cs @@ -1,47 +1,258 @@ +using System.CommandLine; +using System.CommandLine.Invocation; using System.Composition; +using System.Globalization; +using System.Text; using FluentResults; +using Telegram.Bot.Types; +using TelegramBotApp.Application.Factories; +using TelegramBotApp.Application.Services; +using TelegramBotApp.Caching.Caching; +using TelegramBotApp.Domain.Models; using TelegramBotApp.Messaging; +using TelegramBotApp.Messaging.Common; +using TelegramBotApp.Messaging.IntegrationContext.UserSubscriptionEvents; using TelegramBotApp.Messaging.IntegrationContext.WeatherForecastIntegrationEvents; using TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; +// ReSharper disable UnusedType.Global + namespace TelegramBotApp.Application.TelegramCommands; [Export(typeof(ITelegramCommand))] [ExportMetadata(nameof(Command), "/help")] +[ExportMetadata(nameof(Description), "- get help")] public class HelpTelegramCommand : ITelegramCommand { public string Command => "/help"; + public string Description => "- get help"; + public IEnumerable> Arguments => Enumerable.Empty>(); - public Task> Execute(string command, string value, IEventBus bus, + public Task> Execute(InvocationContext context, + long telegramId, + Func> getArgument, + IEventBus bus, ICacheService cacheService, IResendMessageService messageService, CancellationToken cancellationToken) { - if (command != Command) throw new InvalidOperationException($"Invalid command {command}"); + var message = new StringBuilder(); + message.AppendLine("Hello! I'm a weather bot. I can provide you with the current weather in your city."); + message.AppendLine("You can use the following commands:"); + + foreach (var command in TelegramCommandFactory.GetCommands()) + { + message.AppendLine(command); + } - return Task.FromResult(Result.Ok(""" - Hello! I'm a weather bot. I can provide you with the current weather in your city. - You can use the following commands: - /weather - get the current weather in the specified city - /help - get help - """)); + return Task.FromResult(Result.Ok(message.ToString())); } } [Export(typeof(ITelegramCommand))] [ExportMetadata(nameof(Command), "/weather")] +[ExportMetadata(nameof(Description), " - get the current weather in the specified city")] public class WeatherTelegramCommand : ITelegramCommand { public string Command => "/weather"; + public string Description => " - get the current weather in the specified city"; + + public IEnumerable> Arguments + { + get { yield return new("city", "City name"); } + } + + public async Task> Execute(InvocationContext context, + long telegramId, + Func> getArgument, + IEventBus bus, ICacheService cacheService, IResendMessageService messageService, + CancellationToken cancellationToken) + { + var city = context.ParseResult.GetValueForArgument(getArgument("city")); + var response = await bus.Publish(new WeatherForecastRequestIntegrationEvent(city), nameof(UniversalResponse), + CancellationToken.None); // TODO: fix cancellation token + + return !response.IsEmpty + ? Result.Ok(response.Message) + : Result.Fail("An error occurred while processing the request"); + } +} + +[Export(typeof(ITelegramCommand))] +[ExportMetadata(nameof(Command), "/createSubscription")] +[ExportMetadata(nameof(Description), " - create a subscription")] +public class CreateSubscriptionTelegramCommand : ITelegramCommand +{ + public string Command => "/createSubscription"; + public string Description => " - create a subscription"; + + public IEnumerable> Arguments + { + get + { + yield return new("location", "Location"); + yield return new("resendInterval", "Resend interval"); + } + } - public async Task> Execute(string command, string value, IEventBus bus, + public async Task> Execute(InvocationContext context, + long telegramId, + Func> getArgument, + IEventBus bus, ICacheService cacheService, + IResendMessageService messageService, CancellationToken cancellationToken) { - if (command != Command) throw new InvalidOperationException($"Invalid command {command}"); + var location = context.ParseResult.GetValueForArgument(getArgument("location")); + + if (!TimeSpan.TryParseExact(context.ParseResult.GetValueForArgument(getArgument("resendInterval")), + @"hh\:mm\:ss", CultureInfo.InvariantCulture, out var resendInterval)) + { + return "Invalid format for resend interval. Please use the format hh:mm:ss".ToResult(); + } + + if (resendInterval < TimeSpan.FromMinutes(30)) // hardcode + { + return "Resend interval should be at least 30 minutes".ToResult(); // TODO: add validation, result fail + } - var response = await bus.Publish(new WeatherForecastRequestIntegrationEvent(value), nameof(UniversalResponse), - cancellationToken); + messageService.AddOrUpdateResendProcess(telegramId, location, resendInterval); - return response != null + var response = await bus.Publish(new CreatedUserSubscriptionIntegrationEvent + { + TelegramId = telegramId, + ResendInterval = resendInterval, + City = location + }, + nameof(UniversalResponse), CancellationToken.None); // TODO: fix cancellation token + + return !response.IsEmpty ? Result.Ok(response.Message) : Result.Fail("An error occurred while processing the request"); } -} \ No newline at end of file +} + +[Export(typeof(ITelegramCommand))] +[ExportMetadata(nameof(Command), "/getSubscriptions")] +[ExportMetadata(nameof(Description), "- get all subscriptions")] +public class GetUserWeatherSubscriptionsTelegramCommand : ITelegramCommand +{ + public string Command => "/getSubscriptions"; + public string Description => "- get all subscriptions"; + + public IEnumerable> Arguments => Enumerable.Empty>(); + + public async Task> Execute(InvocationContext context, + long telegramId, + Func> getArgument, + IEventBus bus, ICacheService cacheService, IResendMessageService messageService, + CancellationToken cancellationToken) + { + var allSubscriptions = + await cacheService.GetAsync>("allSubscriptions", + CancellationToken.None); // TODO: fix cancellation token + + if (allSubscriptions == null) return Result.Fail("Bad internal state"); + + var message = new StringBuilder(); + + for (var i = 0; i < allSubscriptions.Count; i++) + { + var subscription = allSubscriptions[i]; + message.AppendLine( + $"{i + 1}) Location: {subscription.Location}, resend interval: {subscription.ResendInterval}"); + } + + return message.Length > 0 + ? Result.Ok(message.ToString()) + : Result.Ok("No subscriptions found"); + } +} + +[Export(typeof(ITelegramCommand))] +[ExportMetadata(nameof(Command), "/updateSubscription")] +[ExportMetadata(nameof(Description), " - update a subscription")] +public class UpdateSubscriptionTelegramCommand : ITelegramCommand +{ + public string Command => "/updateSubscription"; + public string Description => " - update a subscription"; + + public IEnumerable> Arguments + { + get + { + yield return new("location", "Location"); + yield return new("resendInterval", "Resend interval"); + } + } + + public async Task> Execute(InvocationContext context, + long telegramId, + Func> getArgument, + IEventBus bus, ICacheService cacheService, IResendMessageService messageService, + CancellationToken cancellationToken) + { + var location = context.ParseResult.GetValueForArgument(getArgument("location")); + + if (!TimeSpan.TryParseExact(context.ParseResult.GetValueForArgument(getArgument("resendInterval")), + @"hh\:mm\:ss", CultureInfo.InvariantCulture, out var resendInterval)) + { + return "Invalid format for resend interval. Please use the format hh:mm:ss".ToResult(); + } + + if (resendInterval < TimeSpan.FromMinutes(30)) // hardcode + { + return "Resend interval should be at least 30 minutes".ToResult(); // TODO: add validation, result fail + } + + messageService.AddOrUpdateResendProcess(telegramId, location, resendInterval); + + var response = await bus.Publish(new UpdatedUserSubscriptionIntegrationEvent + { + TelegramUserId = telegramId, + Location = location, + ResendInterval = resendInterval + }, nameof(UniversalResponse), CancellationToken.None); // TODO: fix cancellation token + + return !response.IsEmpty + ? Result.Ok(response.Message) + : Result.Fail("An error occurred while processing the request"); + } +} + +[Export(typeof(ITelegramCommand))] +[ExportMetadata(nameof(Command), "/deleteSubscription")] +[ExportMetadata(nameof(Description), " - delete a subscription")] +public class DeleteSubscriptionTelegramCommand : ITelegramCommand +{ + public string Command => "/deleteSubscription"; + public string Description => " - delete a subscription"; + + public IEnumerable> Arguments + { + get { yield return new("location", "Location"); } + } + + public async Task> Execute(InvocationContext context, + long telegramId, + Func> getArgument, + IEventBus bus, ICacheService cacheService, IResendMessageService messageService, + CancellationToken cancellationToken) + { + var location = context.ParseResult.GetValueForArgument(getArgument("location")); + + messageService.RemoveResendProcess(telegramId, location); + + var response = await bus.Publish(new DeletedUserSubscriptionIntegrationEvent + { + TelegramUserId = telegramId, + Location = location + }, + nameof(UniversalResponse), CancellationToken.None); // TODO: fix cancellation token + + return !response.IsEmpty + ? Result.Ok(response.Message) + : Result.Fail("An error occurred while processing the request"); + } +} + +/*[Export(typeof(ITelegramCommand))] TODO: create command +[ExportMetadata(nameof(Command), "/deleteAllSubscriptions")] +[ExportMetadata(nameof(Description), "delete all subscriptions")]*/ \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Domain/Models/Interfaces.cs b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Domain/Models/Interfaces.cs index 02db8da..9303497 100644 --- a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Domain/Models/Interfaces.cs +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Domain/Models/Interfaces.cs @@ -1,17 +1,33 @@ using Telegram.Bot.Polling; using Telegram.Bot.Types; +using TelegramBotApp.Caching.Caching; using TelegramBotApp.Messaging; namespace TelegramBotApp.Domain.Models; -public interface IBotInitializer +public interface ITelegramBotSettings { - ITelegramBot CreateBot(string token); + public TimeSpan Timeout { get; } + public CancellationToken Token { get; } +} + +public interface ITelegramBotInitializer +{ + ITelegramBot CreateBot(string token, ReceiverOptions receiverOptions); ReceiverOptions CreateReceiverOptions(); } public interface ITelegramBot { - void StartReceiving(ReceiverOptions receiverOptions, IEventBus bus, CancellationToken cancellationToken); + void StartReceiving(IEventBus bus, ICacheService cacheService, IResendMessageService messageService, + CancellationToken cancellationToken); + + Task SendMessageAsync(long telegramId, string message); Task GetMeAsync(); +} + +public interface IResendMessageService +{ + void AddOrUpdateResendProcess(long telegramId, string location, TimeSpan interval); + void RemoveResendProcess(long telegramId, string location); } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Domain/TelegramBotApp.Domain.csproj b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Domain/TelegramBotApp.Domain.csproj index 5d7cfb0..15d480b 100644 --- a/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Domain/TelegramBotApp.Domain.csproj +++ b/WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Domain/TelegramBotApp.Domain.csproj @@ -1,17 +1,17 @@  - - net8.0 - enable - enable - + + net8.0 + enable + enable + - - - + + + - - - + + + diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/Caching/CacheService.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/Caching/CacheService.cs index f9eade6..04cadac 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/Caching/CacheService.cs +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/Caching/CacheService.cs @@ -8,7 +8,6 @@ public class CacheService(IDistributedCache distributedCache) : ICacheService public async Task GetAsync(string key, CancellationToken cancellationToken = default) where T : class { var value = await distributedCache.GetStringAsync(key, cancellationToken); - return value == null ? null : JsonSerializer.Deserialize(value); } diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/DependencyInjection.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/DependencyInjection.cs index 2dbe3ce..5c7b780 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/DependencyInjection.cs +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/DependencyInjection.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using TelegramBotApp.Caching.Caching; @@ -6,9 +7,12 @@ namespace TelegramBotApp.Caching; public static class DependencyInjection { // ReSharper disable once UnusedMethodReturnValue.Global - public static IServiceCollection AddCaching(this IServiceCollection services) + public static IServiceCollection AddCaching(this IServiceCollection services, IConfiguration configuration) { - services.AddSingleton(); + services.AddStackExchangeRedisCache(options => + options.Configuration = configuration.GetConnectionString("Redis")) + .AddSingleton(); + return services; } } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/TelegramBotApp.Caching.csproj b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/TelegramBotApp.Caching.csproj index 8ed39ad..f731424 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/TelegramBotApp.Caching.csproj +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/TelegramBotApp.Caching.csproj @@ -8,6 +8,7 @@ + diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Common/UserSubcriptionInfo.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Common/UserSubcriptionInfo.cs new file mode 100644 index 0000000..97a49da --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Common/UserSubcriptionInfo.cs @@ -0,0 +1,8 @@ +namespace TelegramBotApp.Messaging.Common; + +public class UserSubscriptionInfo +{ + public long TelegramId { get; init; } + public TimeSpan ResendInterval { get; init; } + public required string Location { get; init; } +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/UniversalPolymorphicTypeResolver.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/CompositionPolymorphicTypeResolver.cs similarity index 100% rename from WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/UniversalPolymorphicTypeResolver.cs rename to WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/CompositionPolymorphicTypeResolver.cs diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Connection/PersistentConnection.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Connection/PersistentConnection.cs index ea502f6..61b0590 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Connection/PersistentConnection.cs +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Connection/PersistentConnection.cs @@ -3,11 +3,12 @@ using Polly; using RabbitMQ.Client; using RabbitMQ.Client.Exceptions; +using TelegramBotApp.Messaging.Settings; namespace TelegramBotApp.Messaging.Connection; public sealed class PersistentConnection( - // IConfiguration configuration, + IMessageSettings messageSettings, ILogger logger, int retryCount = 3) : IPersistentConnection @@ -29,16 +30,30 @@ public bool TryConnect() (ex, time) => logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s ({ExceptionMessage})", $"{time.TotalSeconds:n1}", ex.Message)); - - policy.Execute(() => + + ConnectionFactory connectionFactory; + + if (messageSettings.HasConnectionString) { - var connectionFactory = new ConnectionFactory + connectionFactory = new() { - HostName = "localhost", + Uri = new(messageSettings.ConnectionString!), + DispatchConsumersAsync = true + }; + } + else + { + connectionFactory = new() + { + HostName = messageSettings.HostName, + Port = messageSettings.Port, + UserName = messageSettings.Username, + Password = messageSettings.Password, DispatchConsumersAsync = true }; - _connection = connectionFactory.CreateConnection(); - }); + } + + policy.Execute(() => _connection = connectionFactory.CreateConnection()); if (_connection == null) { diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/DependencyInjection.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/DependencyInjection.cs index 03f09fd..659c9d3 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/DependencyInjection.cs +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/DependencyInjection.cs @@ -17,10 +17,8 @@ public static IServiceCollection AddMessaging(this IServiceCollection services, services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(_ => settings ?? new RabbitMqSettings - { - HostName = "localhost" - }); + services.AddSingleton(_ => + settings ?? throw new InvalidOperationException("RabbitMqSettings not found")); services.AddSingleton(); var interfaceType = typeof(IIntegrationEventHandler); diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/EventBusContext/EventBus.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/EventBusContext/EventBus.cs index 747f235..77a7c76 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/EventBusContext/EventBus.cs +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/EventBusContext/EventBus.cs @@ -16,6 +16,7 @@ namespace TelegramBotApp.Messaging.EventBusContext; public class EventBus( IPersistentConnection persistentConnection, + IMessageSettings messageSettings, ILogger logger, IEventBusSubscriptionsManager subscriptionsManager, IServiceProvider serviceProvider, @@ -23,19 +24,15 @@ public class EventBus( int retryCount = 3) : IEventBus { - private const string DirectEventExchange = "direct-event-exchange"; - private const string DirectResponseExchange = "direct-response-exchange"; - private const string EventQueue = "event-queue"; - private const string ResponseQueue = "response-queue"; - - private IModel _channel = null!; + private IModel? _channel; private readonly ConcurrentDictionary> _callbackMapper = new(); - private bool _isInitialized; + private bool _isStandartQueueInitialized; + private bool _isResponseQueueInitialized; public Task Publish(IntegrationEventBase eventBase, string? replyTo = null, CancellationToken cancellationToken = default) { - if (!_isInitialized) EnsureInitialize(); + if (_channel == null) throw new InvalidOperationException("RabbitMQ channel is not initialized"); var policy = Policy.Handle() .Or() @@ -46,6 +43,7 @@ public Task Publish(IntegrationEventBase eventBase, string? r var body = JsonSerializer.SerializeToUtf8Bytes(eventBase, jsonOptions.Options); var tcs = new TaskCompletionSource(); + var properties = _channel.CreateBasicProperties(); properties.CorrelationId = eventBase.Id.ToString(); properties.ReplyTo = replyTo; @@ -61,7 +59,7 @@ public Task Publish(IntegrationEventBase eventBase, string? r { logger.LogInformation("Publishing event to RabbitMQ: {EventName}", eventBase.Name); _channel.BasicPublish( - exchange: DirectEventExchange, + exchange: messageSettings.EventExchangeName, routingKey: eventBase.Name, basicProperties: properties, body: body); @@ -74,14 +72,14 @@ public void Subscribe() where T : IntegrationEventBase where TH : IIntegrationEventHandler { - if (!_isInitialized) EnsureInitialize(); + if (!_isStandartQueueInitialized) EnsureBasicInitialize(); var eventName = subscriptionsManager.GetEventKey(); if (subscriptionsManager.HasSubscriptionsForEvent(eventName)) return; - _channel.QueueBind(queue: EventQueue, - exchange: DirectEventExchange, + _channel.QueueBind(queue: messageSettings.EventQueueName, + exchange: messageSettings.EventExchangeName, routingKey: eventName); logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, @@ -96,14 +94,14 @@ public void SubscribeResponse() where T : IResponseMessage where TH : IResponseHandler { - if (!_isInitialized) EnsureInitialize(); + if (!_isResponseQueueInitialized) InitializeResponseQueue(); var replyName = subscriptionsManager.GetEventKey(); if (subscriptionsManager.HasSubscriptionsForResponse(replyName)) return; - _channel.QueueBind(queue: ResponseQueue, - exchange: DirectResponseExchange, + _channel.QueueBind(queue: messageSettings.ResponseQueueName, + exchange: messageSettings.ResponseExchangeName, routingKey: replyName); logger.LogInformation("Subscribing to response {ResponseName}", replyName); @@ -111,7 +109,7 @@ public void SubscribeResponse() subscriptionsManager.AddResponseSubscription(); var responseConsumer = new AsyncEventingBasicConsumer(_channel); - + // TODO: avoid code duplication responseConsumer.Received += async (_, ea) => { @@ -160,44 +158,31 @@ public void SubscribeResponse() } }; - _channel.BasicConsume(queue: ResponseQueue, autoAck: true, consumer: responseConsumer); + _channel.BasicConsume(queue: messageSettings.ResponseQueueName, autoAck: true, consumer: responseConsumer); + _isResponseQueueInitialized = true; } - private void EnsureInitialize() + private void EnsureBasicInitialize() { - if (!persistentConnection.IsConnected) - { - if (!persistentConnection.TryConnect()) - { - logger.LogError("Could not connect to RabbitMQ"); - return; - } - } - - _channel = persistentConnection.CreateModel(); - - _channel.ExchangeDeclare(exchange: DirectEventExchange, type: ExchangeType.Direct); - _channel.ExchangeDeclare(exchange: DirectResponseExchange, type: ExchangeType.Direct); + if (!TryConnect()) return; + _channel ??= persistentConnection.CreateModel(); + _channel.ExchangeDeclare(exchange: messageSettings.EventExchangeName, type: ExchangeType.Direct); _channel.QueueDeclare( - queue: EventQueue, - durable: true, - exclusive: false, - autoDelete: true, - arguments: null); - _channel.QueueDeclare( - queue: ResponseQueue, + queue: messageSettings.EventQueueName, durable: true, exclusive: false, autoDelete: true, arguments: null); _channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false); - _isInitialized = true; + _isStandartQueueInitialized = true; } private void StartConsume() { + if (_channel == null) throw new InvalidOperationException("RabbitMQ channel is not initialized"); + var eventConsumer = new AsyncEventingBasicConsumer(_channel); eventConsumer.Received += async (_, ea) => @@ -227,17 +212,15 @@ private void StartConsume() var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); var task = concreteType.GetMethod("Handle") - ?.Invoke(handler, [integrationEvent]) as Task ?? + ?.Invoke(handler, [integrationEvent]) as Task ?? throw new InvalidCastException(); var response = await task; - if (response == null) continue; - var properties = _channel.CreateBasicProperties(); properties.CorrelationId = ea.BasicProperties.CorrelationId; _channel.BasicPublish( - exchange: DirectResponseExchange, + exchange: messageSettings.ResponseExchangeName, routingKey: ea.BasicProperties.ReplyTo, basicProperties: properties, body: JsonSerializer.SerializeToUtf8Bytes(response, jsonOptions.Options)); @@ -250,6 +233,30 @@ private void StartConsume() } }; - _channel.BasicConsume(queue: EventQueue, autoAck: false, consumer: eventConsumer); + _channel.BasicConsume(queue: messageSettings.EventQueueName, autoAck: false, consumer: eventConsumer); + } + + private void InitializeResponseQueue() + { + if (!TryConnect()) return; + + _channel ??= persistentConnection.CreateModel(); + _channel.ExchangeDeclare(exchange: messageSettings.ResponseExchangeName, type: ExchangeType.Direct); + _channel.QueueDeclare( + queue: messageSettings.ResponseQueueName, + durable: true, + exclusive: false, + autoDelete: true, + arguments: null); + } + + private bool TryConnect() + { + if (persistentConnection.IsConnected) return true; + if (persistentConnection.TryConnect()) return true; + + logger.LogError("Could not connect to RabbitMQ"); + + return false; } } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/EventBusExtensions.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/EventBusExtensions.cs index ed4780b..95aa175 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/EventBusExtensions.cs +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/EventBusExtensions.cs @@ -11,10 +11,9 @@ public static class EventBusExtensions public static IHost SubscribeToResponses(this IHost app) { var eventBus = app.Services.GetRequiredService(); - + eventBus.SubscribeResponse(); - eventBus.SubscribeResponse(); - + return app; } } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/Interfaces.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/Interfaces.cs index a22b03d..fc7411b 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/Interfaces.cs +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/Interfaces.cs @@ -7,5 +7,5 @@ public interface IIntegrationEventHandler; public interface IIntegrationEventHandler : IIntegrationEventHandler where TIntegrationEvent : IntegrationEventBase { - Task Handle(TIntegrationEvent @event); + Task Handle(TIntegrationEvent @event); } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/CacheRequestUserSubscriptionsIntegrationEvent.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/CacheRequestUserSubscriptionsIntegrationEvent.cs new file mode 100644 index 0000000..ff23967 --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/CacheRequestUserSubscriptionsIntegrationEvent.cs @@ -0,0 +1,6 @@ +namespace TelegramBotApp.Messaging.IntegrationContext.UserIntegrationEvents; + +public class CacheRequestUserSubscriptionsIntegrationEvent : IntegrationEventBase +{ + public override string Name => nameof(CacheRequestUserSubscriptionsIntegrationEvent); +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/CreatedUserIntegrationEvent.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/CreatedUserIntegrationEvent.cs index 4010d3f..2f33408 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/CreatedUserIntegrationEvent.cs +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/CreatedUserIntegrationEvent.cs @@ -1,10 +1,10 @@ namespace TelegramBotApp.Messaging.IntegrationContext.UserIntegrationEvents; -public class CreatedUserIntegrationEventBase : IntegrationEventBase +public class CreatedUserIntegrationEvent : IntegrationEventBase { - public override string Name => nameof(CreatedUserIntegrationEventBase); + public override string Name => nameof(CreatedUserIntegrationEvent); - public int UserTelegramId { get; init; } + public long UserTelegramId { get; init; } public required string Username { get; init; } public required string MobileNumber { get; init; } public DateTime RegisteredAt { get; init; } diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/GetAllUsersRequestIntegrationEvent.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/GetAllUsersRequestIntegrationEvent.cs deleted file mode 100644 index 181edb3..0000000 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/GetAllUsersRequestIntegrationEvent.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TelegramBotApp.Messaging.IntegrationContext.UserIntegrationEvents; - -public class GetAllUsersRequestIntegrationEvent : IntegrationEventBase -{ - public override string Name => nameof(GetAllUsersRequestIntegrationEvent); -} \ No newline at end of file diff --git "a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/\320\241acheRequestUsersIntegrationEvent.cs" "b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/\320\241acheRequestUsersIntegrationEvent.cs" new file mode 100644 index 0000000..0f5575a --- /dev/null +++ "b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserIntegrationEvents/\320\241acheRequestUsersIntegrationEvent.cs" @@ -0,0 +1,6 @@ +namespace TelegramBotApp.Messaging.IntegrationContext.UserIntegrationEvents; + +public class CacheRequestUsersIntegrationEvent : IntegrationEventBase +{ + public override string Name => nameof(CacheRequestUsersIntegrationEvent); +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/CreatedUserSubscriptionIntegrationEvent.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/CreatedUserSubscriptionIntegrationEvent.cs new file mode 100644 index 0000000..26f3f12 --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/CreatedUserSubscriptionIntegrationEvent.cs @@ -0,0 +1,9 @@ +namespace TelegramBotApp.Messaging.IntegrationContext.UserSubscriptionEvents; + +public class CreatedUserSubscriptionIntegrationEvent : IntegrationEventBase +{ + public override string Name => nameof(CreatedUserSubscriptionIntegrationEvent); + public long TelegramId { get; init; } + public required string City { get; init; } + public TimeSpan ResendInterval { get; init; } +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/DeletedUserSubscriptionIntegrationEvent.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/DeletedUserSubscriptionIntegrationEvent.cs new file mode 100644 index 0000000..1a08ed5 --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/DeletedUserSubscriptionIntegrationEvent.cs @@ -0,0 +1,8 @@ +namespace TelegramBotApp.Messaging.IntegrationContext.UserSubscriptionEvents; + +public class DeletedUserSubscriptionIntegrationEvent : IntegrationEventBase +{ + public override string Name => nameof(DeletedUserSubscriptionIntegrationEvent); + public long TelegramUserId { get; init; } + public required string Location { get; init; } +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/UpdatedUserSubscriptionIntegrationEvent.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/UpdatedUserSubscriptionIntegrationEvent.cs new file mode 100644 index 0000000..660b82a --- /dev/null +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/UserSubscriptionEvents/UpdatedUserSubscriptionIntegrationEvent.cs @@ -0,0 +1,9 @@ +namespace TelegramBotApp.Messaging.IntegrationContext.UserSubscriptionEvents; + +public class UpdatedUserSubscriptionIntegrationEvent : IntegrationEventBase +{ + public override string Name => nameof(UpdatedUserSubscriptionIntegrationEvent); + public long TelegramUserId { get; init; } + public required string Location { get; init; } + public TimeSpan ResendInterval { get; init; } +} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/WeatherForecastIntegrationEvents/RequestWeatherForecastIntegrationEvent.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/WeatherForecastIntegrationEvents/WeatherForecastRequestIntegrationEvent.cs similarity index 100% rename from WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/WeatherForecastIntegrationEvents/RequestWeatherForecastIntegrationEvent.cs rename to WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationContext/WeatherForecastIntegrationEvents/WeatherForecastRequestIntegrationEvent.cs diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponseHandlers/AllUserResponseHandler.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponseHandlers/AllUserResponseHandler.cs deleted file mode 100644 index 101e8bd..0000000 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponseHandlers/AllUserResponseHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -using TelegramBotApp.Caching.Caching; -using TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; - -namespace TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponseHandlers; - -public class AllUserResponseHandler(ICacheService cacheService) : IResponseHandler -{ - public async Task Handle(AllUsersResponse response) - { - await cacheService.RemoveAsync("allUsers"); - - await cacheService.SetAsync("allUsers", response.UserTelegramIds); - - return UniversalResponse.Empty; - } -} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponseHandlers/UniversalResponseHandler.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponseHandlers/UniversalResponseHandler.cs index 9007076..bba1bf4 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponseHandlers/UniversalResponseHandler.cs +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponseHandlers/UniversalResponseHandler.cs @@ -4,5 +4,5 @@ namespace TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationRespons public class UniversalResponseHandler : IResponseHandler { - public Task Handle(UniversalResponse response) => Task.FromResult(response)!; + public Task Handle(UniversalResponse response) => Task.FromResult(response); } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponses/AllUsersResponse.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponses/AllUsersResponse.cs deleted file mode 100644 index 3374563..0000000 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/IntegrationResponseContext/IntegrationResponses/AllUsersResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; - -public class AllUsersResponse : IResponseMessage -{ - public required List UserTelegramIds { get; init; } -} \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Settings/RabbitMqSettings.cs b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Settings/RabbitMqSettings.cs index 269050c..271b883 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Settings/RabbitMqSettings.cs +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/Settings/RabbitMqSettings.cs @@ -1,11 +1,29 @@ namespace TelegramBotApp.Messaging.Settings; -public interface ISettings +public interface IMessageSettings { - string HostName { get; } + string? HostName { get; } + int Port { get; } + string? Username { get; } + string? Password { get; } + string EventQueueName { get; } + string ResponseQueueName { get; } + string EventExchangeName { get; } + string ResponseExchangeName { get; } + string? ConnectionString { get; } + bool HasConnectionString { get; } } -public class RabbitMqSettings : ISettings +public class RabbitMqSettings : IMessageSettings { - public required string HostName { get; init; } + public string? HostName { get; init; } + public int Port { get; init; } + public string? Username { get; init; } + public string? Password { get; init; } + public required string EventQueueName { get; init; } + public required string ResponseQueueName { get; init; } + public required string EventExchangeName { get; init; } + public required string ResponseExchangeName { get; init; } + public string? ConnectionString { get; init; } + public bool HasConnectionString => !string.IsNullOrWhiteSpace(ConnectionString); } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/TelegramBotApp.Messaging.csproj b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/TelegramBotApp.Messaging.csproj index 9a6e737..6736f77 100644 --- a/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/TelegramBotApp.Messaging.csproj +++ b/WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/TelegramBotApp.Messaging.csproj @@ -15,7 +15,7 @@ - + diff --git a/WeatherBotApi.TelegramBotApp/Presentation/TelegramBotApp.Api/AppPipeline/AppPipeline.cs b/WeatherBotApi.TelegramBotApp/Presentation/TelegramBotApp.Api/AppPipeline/AppPipeline.cs index 4d1974d..b98d2b6 100644 --- a/WeatherBotApi.TelegramBotApp/Presentation/TelegramBotApp.Api/AppPipeline/AppPipeline.cs +++ b/WeatherBotApi.TelegramBotApp/Presentation/TelegramBotApp.Api/AppPipeline/AppPipeline.cs @@ -1,9 +1,12 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using TelegramBotApp.Application.TelegramBotContext; +using TelegramBotApp.Application; using TelegramBotApp.Caching; +using TelegramBotApp.Caching.Caching; +using TelegramBotApp.Domain.Models; using TelegramBotApp.Messaging; +using TelegramBotApp.Messaging.Common; using TelegramBotApp.Messaging.IntegrationContext.UserIntegrationEvents; using TelegramBotApp.Messaging.IntegrationResponseContext.IntegrationResponses; @@ -19,25 +22,27 @@ public async Task Run() .ConfigureAppConfiguration(config => config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)) .ConfigureServices((builder, services) => services + .AddApplication(builder.Configuration) .AddMessaging(builder.Configuration) .AddResponseHandlers() - .AddStackExchangeRedisCache(options => - options.Configuration = builder.Configuration.GetConnectionString("Redis")) - .AddCaching()) + .AddCaching(builder.Configuration)) .Build() .SubscribeToResponses(); - var botInitializer = new TelegramBotInitializer(); - var botClient = botInitializer.CreateBot(host.Services.GetRequiredService() - .GetRequiredSection("TelegramSettings:BotToken").Value ?? - throw new InvalidOperationException("Bot token is not set.")); var cancellationTokenSource = new CancellationTokenSource(); var eventBus = host.Services.GetRequiredService(); + var botClient = host.Services.GetRequiredService(); + var cacheService = host.Services.GetRequiredService(); + var resendMessageService = host.Services.GetRequiredService(); - UpdateCache(eventBus); + await UpdateCache(eventBus); + await InitializeResendMessageService(resendMessageService, cacheService); - botClient.StartReceiving(botInitializer.CreateReceiverOptions(), - host.Services.GetRequiredService(), cancellationTokenSource.Token); + botClient.StartReceiving( + eventBus, + cacheService, + resendMessageService, + cancellationTokenSource.Token); var me = await botClient.GetMeAsync(); @@ -53,6 +58,22 @@ public async Task Run() } } - private static void UpdateCache(IEventBus bus) => - _ = bus.Publish(new GetAllUsersRequestIntegrationEvent(), replyTo: nameof(AllUsersResponse)); + private static async Task UpdateCache(IEventBus bus) + { + var task1 = bus.Publish(new CacheRequestUsersIntegrationEvent(), + replyTo: nameof(UniversalResponse)); // TODO: fix cancellation token (common settings for bus), fix reply + var task2 = bus.Publish(new CacheRequestUserSubscriptionsIntegrationEvent(), + replyTo: nameof(UniversalResponse)); + + await Task.WhenAll(task1, task2); + } + + private static async Task InitializeResendMessageService(IResendMessageService messageService, + ICacheService cacheService) + { + var subscriptionInfos = await cacheService.GetAsync>("allSubscriptions"); + if (subscriptionInfos?.Count == 0) return; + subscriptionInfos?.ForEach(s => + messageService.AddOrUpdateResendProcess(s.TelegramId, s.Location, s.ResendInterval)); + } } \ No newline at end of file diff --git a/WeatherBotApi.TelegramBotApp/Presentation/TelegramBotApp.Api/appsettings.json b/WeatherBotApi.TelegramBotApp/Presentation/TelegramBotApp.Api/appsettings.json index a82d938..861811d 100644 --- a/WeatherBotApi.TelegramBotApp/Presentation/TelegramBotApp.Api/appsettings.json +++ b/WeatherBotApi.TelegramBotApp/Presentation/TelegramBotApp.Api/appsettings.json @@ -5,7 +5,14 @@ } }, "RabbitMqSettings": { - "HostName": "localhost" + "HostName": "localhost", + "Port": 15672, + "Username": "guest", + "Password": "guest", + "EventQueueName": "telegram-bot-event-queue", + "ResponseQueueName": "telegram-bot-response-queue", + "EventExchangeName": "event-exchange", + "ResponseExchangeName": "response-exchange" }, "TelegramSettings": { "BotToken": "6819090366:AAEp-IrmVXY-U2Ie91lZktlkjxPG1IkJTJU" diff --git a/WeatherBotApi.WeatherApp/Core/WeatherApp.Application/MessageFormatters/WeatherDescriptorFormatter.cs b/WeatherBotApi.WeatherApp/Core/WeatherApp.Application/MessageFormatters/WeatherDescriptorFormatter.cs index e19c92b..faefb8b 100644 --- a/WeatherBotApi.WeatherApp/Core/WeatherApp.Application/MessageFormatters/WeatherDescriptorFormatter.cs +++ b/WeatherBotApi.WeatherApp/Core/WeatherApp.Application/MessageFormatters/WeatherDescriptorFormatter.cs @@ -7,6 +7,7 @@ public class WeatherDescriptorFormatter : IMessageFormatter { public string Format(WeatherDescriptor value) => $""" + Location: {value.Location} Temperature: {value.Temperature}°C Feels Like: {value.FeelTemperature}°C Humidity: {value.Humidity}% diff --git a/WeatherBotApi.WeatherApp/Core/WeatherApp.Domain/Models/WeatherDescriptor.cs b/WeatherBotApi.WeatherApp/Core/WeatherApp.Domain/Models/WeatherDescriptor.cs index 6c6fcd3..b9a96d9 100644 --- a/WeatherBotApi.WeatherApp/Core/WeatherApp.Domain/Models/WeatherDescriptor.cs +++ b/WeatherBotApi.WeatherApp/Core/WeatherApp.Domain/Models/WeatherDescriptor.cs @@ -2,6 +2,7 @@ public class WeatherDescriptor { + public string? Location { get; private set; } public required int Temperature { get; init; } public required int FeelTemperature { get; init; } public required int Humidity { get; init; } @@ -9,4 +10,6 @@ public class WeatherDescriptor public required int Visibility { get; init; } public required int Wind { get; init; } public required int UvIndex { get; init; } + + public void SetLocation(string? location) => Location = location; } \ No newline at end of file diff --git a/WeatherBotApi.WeatherApp/Infrastructure/WeatherApp.IntegrationEvents/IntegrationEventHandlers/RequestWeatherForecastIntegrationEventHandler.cs b/WeatherBotApi.WeatherApp/Infrastructure/WeatherApp.IntegrationEvents/IntegrationEventHandlers/RequestWeatherForecastIntegrationEventHandler.cs index 14ef566..083b62f 100644 --- a/WeatherBotApi.WeatherApp/Infrastructure/WeatherApp.IntegrationEvents/IntegrationEventHandlers/RequestWeatherForecastIntegrationEventHandler.cs +++ b/WeatherBotApi.WeatherApp/Infrastructure/WeatherApp.IntegrationEvents/IntegrationEventHandlers/RequestWeatherForecastIntegrationEventHandler.cs @@ -12,7 +12,10 @@ public class WeatherForecastRequestIntegrationEventHandler(IWeatherService weath { private readonly WeatherDescriptorFormatter _messageFormatter = new(); - public async Task Handle(WeatherForecastRequestIntegrationEvent eventBase) => - new UniversalResponse( - _messageFormatter.Format(await weatherService.GetWeatherForecastAsync(eventBase.Location))); + public async Task Handle(WeatherForecastRequestIntegrationEvent @event) + { + var weatherDescriptor = await weatherService.GetWeatherForecastAsync(@event.Location); + weatherDescriptor.SetLocation(@event.Location); + return new UniversalResponse(_messageFormatter.Format(weatherDescriptor)); + } } \ No newline at end of file diff --git a/WeatherBotApi.WeatherApp/Presentation/WeatherApp.WebApi/appsettings.Development.json b/WeatherBotApi.WeatherApp/Presentation/WeatherApp.WebApi/appsettings.Development.json index 503501e..031843d 100644 --- a/WeatherBotApi.WeatherApp/Presentation/WeatherApp.WebApi/appsettings.Development.json +++ b/WeatherBotApi.WeatherApp/Presentation/WeatherApp.WebApi/appsettings.Development.json @@ -6,6 +6,13 @@ } }, "RabbitMqSettings": { - "HostName": "localhost" + "HostName": "localhost", + "Port": 15672, + "Username": "guest", + "Password": "guest", + "EventQueueName": "weather-event-queue", + "ResponseQueueName": "weather-response-queue", + "EventExchangeName": "event-exchange", + "ResponseExchangeName": "response-exchange" } -} +} \ No newline at end of file diff --git a/WeatherBotApi.WeatherApp/Presentation/WeatherApp.WebApi/appsettings.json b/WeatherBotApi.WeatherApp/Presentation/WeatherApp.WebApi/appsettings.json index 2aadfc2..9485e6c 100644 --- a/WeatherBotApi.WeatherApp/Presentation/WeatherApp.WebApi/appsettings.json +++ b/WeatherBotApi.WeatherApp/Presentation/WeatherApp.WebApi/appsettings.json @@ -7,6 +7,13 @@ }, "AllowedHosts": "*", "RabbitMqSettings": { - "HostName": "localhost" + "HostName": "localhost", + "Port": 15672, + "Username": "guest", + "Password": "guest", + "EventQueueName": "weather-event-queue", + "ResponseQueueName": "weather-response-queue", + "EventExchangeName": "event-exchange", + "ResponseExchangeName": "response-exchange" } -} +} \ No newline at end of file diff --git a/WeatherBotApi.WeatherApp/Tests/WeatherApp.Tests/WeatherServiceTests.cs b/WeatherBotApi.WeatherApp/Tests/WeatherApp.Tests/WeatherServiceTests.cs index e27389c..1dc8ba4 100644 --- a/WeatherBotApi.WeatherApp/Tests/WeatherApp.Tests/WeatherServiceTests.cs +++ b/WeatherBotApi.WeatherApp/Tests/WeatherApp.Tests/WeatherServiceTests.cs @@ -27,9 +27,9 @@ public async Task GetWeatherForecast_WithValidLocation_ReturnsOkObjectResult(str // Act var result = await controller.GetWeatherForecast(location); - var okResult = result as OkObjectResult; // Assert + var okResult = result.Should().BeOfType().Subject; okResult.Should().NotBeNull(); okResult?.StatusCode.Should().Be(StatusCodes.Status200OK); } @@ -42,9 +42,9 @@ public async Task GetWeatherForecast_WithInvalidLocation_ReturnsBadRequest() // Act var result = await controller.GetWeatherForecast("InvalidLocation"); - var badRequestResult = result as BadRequestObjectResult; // Assert + var badRequestResult = result.Should().BeOfType().Subject; badRequestResult.Should().NotBeNull(); badRequestResult?.StatusCode.Should().Be(StatusCodes.Status400BadRequest); } diff --git a/docker-compose.yml b/docker-compose.yml index 74c433e..968431c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,5 +61,6 @@ services: - "5755:5432" redis: image: 'redis:latest' + restart: always ports: - "6379:6379" \ No newline at end of file