From f88f340d0a1e3ce37ba1e41eb5fd9ef30a323ab7 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 21 Jan 2026 23:16:31 -0500 Subject: [PATCH] [FA-misc] Mass transit overhaul, needs testing and review --- .../AuthenticationWebhookController.cs | 13 +- .../IntegrationEvents/AuthUserAddedEvent.cs | 16 -- .../Program.cs | 12 +- .../appsettings.json | 6 +- .../FileUploadRequestCreatedEvent.cs | 10 -- .../FileUploadRequestStatusUpdateEvent.cs | 22 --- FictionArchive.Service.FileService/Program.cs | 18 +-- ...andler.cs => UploadFileCommandConsumer.cs} | 39 ++--- .../appsettings.json | 6 +- .../NovelUpdateServiceTests.cs | 53 ++++--- .../GraphQL/Mutation.cs | 13 +- .../ChapterPullRequestedEvent.cs | 10 -- .../FileUploadRequestCreatedEvent.cs | 10 -- .../FileUploadRequestStatusUpdateEvent.cs | 22 --- .../NovelUpdateRequestedEvent.cs | 8 - .../TranslationRequestCompletedEvent.cs | 17 --- .../TranslationRequestCreatedEvent.cs | 13 -- .../Program.cs | 24 +-- .../FileUploadCompletedEventConsumer.cs | 53 +++++++ .../Consumers/ImportNovelCommandConsumer.cs | 26 ++++ .../PullChapterContentCommandConsumer.cs | 28 ++++ .../TranslationCompletedEventConsumer.cs | 52 +++++++ .../ChapterPullRequestedEventHandler.cs | 19 --- ...leUploadRequestStatusUpdateEventHandler.cs | 39 ----- .../NovelUpdateRequestedEventHandler.cs | 23 --- ...TranslationRequestCompletedEventHandler.cs | 39 ----- .../Services/NovelUpdateService.cs | 57 +++++--- .../appsettings.json | 6 +- .../Dockerfile | 23 +++ ...ionArchive.Service.ReportingService.csproj | 20 +++ .../GraphQL/Mutation.cs | 15 ++ .../GraphQL/Query.cs | 52 +++++++ .../Models/DTOs/JobDto.cs | 17 +++ .../Models/DTOs/JobHistoryEntryDto.cs | 12 ++ .../Models/Database/Job.cs | 19 +++ .../Models/Database/JobHistoryEntry.cs | 16 ++ .../Program.cs | 76 ++++++++++ .../Properties/launchSettings.json | 39 +++++ .../Consumers/JobStateChangedEventConsumer.cs | 74 ++++++++++ .../Services/ReportingServiceDbContext.cs | 36 +++++ .../appsettings.Development.json | 8 + .../appsettings.json | 28 ++++ .../subgraph-config.json | 6 + ...ionArchive.Service.SchedulerService.csproj | 1 + .../Models/JobTemplates/EventJobTemplate.cs | 64 ++++++-- .../Program.cs | 9 +- .../Services/JobManagerService.cs | 1 - .../appsettings.json | 6 +- .../FictionArchive.Service.Shared.csproj | 4 +- .../Configuration/MassTransitOptions.cs | 9 ++ .../Contracts/Commands/ImportNovelCommand.cs | 6 + .../Commands/PullChapterContentCommand.cs | 8 + .../Commands/TranslateTextCommand.cs | 12 ++ .../Contracts/Commands/UploadFileCommand.cs | 8 + .../Contracts/Events/AuthUserAddedEvent.cs | 9 ++ .../Contracts/Events}/ChapterCreatedEvent.cs | 6 +- .../Events/FileUploadCompletedEvent.cs | 11 ++ .../Contracts/Events}/NovelCreatedEvent.cs | 5 +- .../Events/TranslationCompletedEvent.cs | 7 + .../Contracts/Events/UserInvitedEvent.cs | 13 ++ .../MassTransit/Contracts/ICommand.cs | 6 + .../MassTransit/Contracts/IEvent.cs | 6 + .../Contracts/JobStateChangedEvent.cs | 18 +++ .../MassTransit/MassTransitExtensions.cs | 103 +++++++++++++ .../Services/EventBus/EventBusBuilder.cs | 25 ---- .../Services/EventBus/EventBusExtensions.cs | 12 -- .../Services/EventBus/IEventBus.cs | 7 - .../Services/EventBus/IIntegrationEvent.cs | 7 - .../EventBus/IIntegrationEventHandler.cs | 12 -- .../RabbitMQConnectionProvider.cs | 35 ----- .../Implementations/RabbitMQEventBus.cs | 137 ------------------ .../Implementations/RabbitMQExtensions.cs | 24 --- .../Implementations/RabbitMQOptions.cs | 7 - .../Services/EventBus/SubscriptionManager.cs | 11 -- ...nArchive.Service.TranslationService.csproj | 1 + .../TranslationRequestCompletedEvent.cs | 18 --- .../TranslationRequestCreatedEvent.cs | 13 -- .../Program.cs | 17 +-- .../TranslateTextCommandConsumer.cs | 40 +++++ .../TranslationRequestCreatedEventHandler.cs | 31 ---- .../Services/TranslationEngineService.cs | 9 +- .../appsettings.json | 6 +- .../IntegrationEvents/ChapterCreatedEvent.cs | 13 -- .../IntegrationEvents/NovelCreatedEvent.cs | 13 -- .../IntegrationEvents/UserInvitedEvent.cs | 15 -- .../Program.cs | 20 +-- ...dler.cs => ChapterCreatedEventConsumer.cs} | 16 +- ...andler.cs => NovelCreatedEventConsumer.cs} | 16 +- ...Handler.cs => UserInvitedEventConsumer.cs} | 16 +- .../appsettings.json | 6 +- .../UserManagementServiceTests.cs | 6 +- .../IntegrationEvents/UserInvitedEvent.cs | 17 --- FictionArchive.Service.UserService/Program.cs | 9 +- .../Services/UserManagementService.cs | 12 +- .../appsettings.json | 6 +- FictionArchive.sln | 8 +- docker-compose.yml | 16 ++ 97 files changed, 1150 insertions(+), 858 deletions(-) delete mode 100644 FictionArchive.Service.AuthenticationService/Models/IntegrationEvents/AuthUserAddedEvent.cs delete mode 100644 FictionArchive.Service.FileService/Models/IntegrationEvents/FileUploadRequestCreatedEvent.cs delete mode 100644 FictionArchive.Service.FileService/Models/IntegrationEvents/FileUploadRequestStatusUpdateEvent.cs rename FictionArchive.Service.FileService/Services/EventHandlers/{FileUploadRequestCreatedEventHandler.cs => UploadFileCommandConsumer.cs} (57%) delete mode 100644 FictionArchive.Service.NovelService/Models/IntegrationEvents/ChapterPullRequestedEvent.cs delete mode 100644 FictionArchive.Service.NovelService/Models/IntegrationEvents/FileUploadRequestCreatedEvent.cs delete mode 100644 FictionArchive.Service.NovelService/Models/IntegrationEvents/FileUploadRequestStatusUpdateEvent.cs delete mode 100644 FictionArchive.Service.NovelService/Models/IntegrationEvents/NovelUpdateRequestedEvent.cs delete mode 100644 FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs delete mode 100644 FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs create mode 100644 FictionArchive.Service.NovelService/Services/Consumers/FileUploadCompletedEventConsumer.cs create mode 100644 FictionArchive.Service.NovelService/Services/Consumers/ImportNovelCommandConsumer.cs create mode 100644 FictionArchive.Service.NovelService/Services/Consumers/PullChapterContentCommandConsumer.cs create mode 100644 FictionArchive.Service.NovelService/Services/Consumers/TranslationCompletedEventConsumer.cs delete mode 100644 FictionArchive.Service.NovelService/Services/EventHandlers/ChapterPullRequestedEventHandler.cs delete mode 100644 FictionArchive.Service.NovelService/Services/EventHandlers/FileUploadRequestStatusUpdateEventHandler.cs delete mode 100644 FictionArchive.Service.NovelService/Services/EventHandlers/NovelUpdateRequestedEventHandler.cs delete mode 100644 FictionArchive.Service.NovelService/Services/EventHandlers/TranslationRequestCompletedEventHandler.cs create mode 100644 FictionArchive.Service.ReportingService/Dockerfile create mode 100644 FictionArchive.Service.ReportingService/FictionArchive.Service.ReportingService.csproj create mode 100644 FictionArchive.Service.ReportingService/GraphQL/Mutation.cs create mode 100644 FictionArchive.Service.ReportingService/GraphQL/Query.cs create mode 100644 FictionArchive.Service.ReportingService/Models/DTOs/JobDto.cs create mode 100644 FictionArchive.Service.ReportingService/Models/DTOs/JobHistoryEntryDto.cs create mode 100644 FictionArchive.Service.ReportingService/Models/Database/Job.cs create mode 100644 FictionArchive.Service.ReportingService/Models/Database/JobHistoryEntry.cs create mode 100644 FictionArchive.Service.ReportingService/Program.cs create mode 100644 FictionArchive.Service.ReportingService/Properties/launchSettings.json create mode 100644 FictionArchive.Service.ReportingService/Services/Consumers/JobStateChangedEventConsumer.cs create mode 100644 FictionArchive.Service.ReportingService/Services/ReportingServiceDbContext.cs create mode 100644 FictionArchive.Service.ReportingService/appsettings.Development.json create mode 100644 FictionArchive.Service.ReportingService/appsettings.json create mode 100644 FictionArchive.Service.ReportingService/subgraph-config.json create mode 100644 FictionArchive.Service.Shared/MassTransit/Configuration/MassTransitOptions.cs create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/Commands/ImportNovelCommand.cs create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/Commands/PullChapterContentCommand.cs create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/Commands/TranslateTextCommand.cs create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/Commands/UploadFileCommand.cs create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/Events/AuthUserAddedEvent.cs rename {FictionArchive.Service.NovelService/Models/IntegrationEvents => FictionArchive.Service.Shared/MassTransit/Contracts/Events}/ChapterCreatedEvent.cs (63%) create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/Events/FileUploadCompletedEvent.cs rename {FictionArchive.Service.NovelService/Models/IntegrationEvents => FictionArchive.Service.Shared/MassTransit/Contracts/Events}/NovelCreatedEvent.cs (62%) create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/Events/TranslationCompletedEvent.cs create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/Events/UserInvitedEvent.cs create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/ICommand.cs create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/IEvent.cs create mode 100644 FictionArchive.Service.Shared/MassTransit/Contracts/JobStateChangedEvent.cs create mode 100644 FictionArchive.Service.Shared/MassTransit/MassTransitExtensions.cs delete mode 100644 FictionArchive.Service.Shared/Services/EventBus/EventBusBuilder.cs delete mode 100644 FictionArchive.Service.Shared/Services/EventBus/EventBusExtensions.cs delete mode 100644 FictionArchive.Service.Shared/Services/EventBus/IEventBus.cs delete mode 100644 FictionArchive.Service.Shared/Services/EventBus/IIntegrationEvent.cs delete mode 100644 FictionArchive.Service.Shared/Services/EventBus/IIntegrationEventHandler.cs delete mode 100644 FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQConnectionProvider.cs delete mode 100644 FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQEventBus.cs delete mode 100644 FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQExtensions.cs delete mode 100644 FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQOptions.cs delete mode 100644 FictionArchive.Service.Shared/Services/EventBus/SubscriptionManager.cs delete mode 100644 FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs delete mode 100644 FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs create mode 100644 FictionArchive.Service.TranslationService/Services/EventHandlers/TranslateTextCommandConsumer.cs delete mode 100644 FictionArchive.Service.TranslationService/Services/EventHandlers/TranslationRequestCreatedEventHandler.cs delete mode 100644 FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/ChapterCreatedEvent.cs delete mode 100644 FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/NovelCreatedEvent.cs delete mode 100644 FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/UserInvitedEvent.cs rename FictionArchive.Service.UserNovelDataService/Services/EventHandlers/{ChapterCreatedEventHandler.cs => ChapterCreatedEventConsumer.cs} (77%) rename FictionArchive.Service.UserNovelDataService/Services/EventHandlers/{NovelCreatedEventHandler.cs => NovelCreatedEventConsumer.cs} (66%) rename FictionArchive.Service.UserNovelDataService/Services/EventHandlers/{UserInvitedEventHandler.cs => UserInvitedEventConsumer.cs} (69%) delete mode 100644 FictionArchive.Service.UserService/Models/IntegrationEvents/UserInvitedEvent.cs diff --git a/FictionArchive.Service.AuthenticationService/Controllers/AuthenticationWebhookController.cs b/FictionArchive.Service.AuthenticationService/Controllers/AuthenticationWebhookController.cs index 7ab6272..30cbdc1 100644 --- a/FictionArchive.Service.AuthenticationService/Controllers/AuthenticationWebhookController.cs +++ b/FictionArchive.Service.AuthenticationService/Controllers/AuthenticationWebhookController.cs @@ -1,7 +1,6 @@ using FictionArchive.Service.AuthenticationService.Models.Requests; -using FictionArchive.Service.AuthenticationService.Models.IntegrationEvents; -using FictionArchive.Service.Shared.Services.EventBus; -using Microsoft.AspNetCore.Http; +using FictionArchive.Service.Shared.MassTransit.Contracts.Events; +using MassTransit; using Microsoft.AspNetCore.Mvc; namespace FictionArchive.Service.AuthenticationService.Controllers @@ -10,11 +9,11 @@ namespace FictionArchive.Service.AuthenticationService.Controllers [ApiController] public class AuthenticationWebhookController : ControllerBase { - private readonly IEventBus _eventBus; + private readonly IPublishEndpoint _publishEndpoint; - public AuthenticationWebhookController(IEventBus eventBus) + public AuthenticationWebhookController(IPublishEndpoint publishEndpoint) { - _eventBus = eventBus; + _publishEndpoint = publishEndpoint; } [HttpPost(nameof(UserRegistered))] @@ -28,7 +27,7 @@ namespace FictionArchive.Service.AuthenticationService.Controllers EventUserUsername = payload.EventUserUsername }; - await _eventBus.Publish(authUserAddedEvent); + await _publishEndpoint.Publish(authUserAddedEvent); return Ok(); } diff --git a/FictionArchive.Service.AuthenticationService/Models/IntegrationEvents/AuthUserAddedEvent.cs b/FictionArchive.Service.AuthenticationService/Models/IntegrationEvents/AuthUserAddedEvent.cs deleted file mode 100644 index 93ace36..0000000 --- a/FictionArchive.Service.AuthenticationService/Models/IntegrationEvents/AuthUserAddedEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.AuthenticationService.Models.IntegrationEvents; - -public class AuthUserAddedEvent : IIntegrationEvent -{ - public string OAuthProviderId { get; set; } - - public string InviterOAuthProviderId { get; set; } - - // The email of the user that created the event - public string EventUserEmail { get; set; } - - // The username of the user that created the event - public string EventUserUsername { get; set; } -} \ No newline at end of file diff --git a/FictionArchive.Service.AuthenticationService/Program.cs b/FictionArchive.Service.AuthenticationService/Program.cs index 897d20e..7ebb5ae 100644 --- a/FictionArchive.Service.AuthenticationService/Program.cs +++ b/FictionArchive.Service.AuthenticationService/Program.cs @@ -1,5 +1,4 @@ -using FictionArchive.Service.Shared; -using FictionArchive.Service.Shared.Services.EventBus.Implementations; +using FictionArchive.Service.Shared.MassTransit; namespace FictionArchive.Service.AuthenticationService; @@ -16,13 +15,10 @@ public class Program builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); - #region Event Bus + #region MassTransit + + builder.Services.AddFictionArchiveMassTransit(builder.Configuration); - builder.Services.AddRabbitMQ(opt => - { - builder.Configuration.GetSection("RabbitMQ").Bind(opt); - }); - #endregion builder.Services.AddHealthChecks(); diff --git a/FictionArchive.Service.AuthenticationService/appsettings.json b/FictionArchive.Service.AuthenticationService/appsettings.json index 9087a1b..6ff0781 100644 --- a/FictionArchive.Service.AuthenticationService/appsettings.json +++ b/FictionArchive.Service.AuthenticationService/appsettings.json @@ -6,8 +6,10 @@ } }, "RabbitMQ": { - "ConnectionString": "amqp://localhost", - "ClientIdentifier": "AuthenticationService" + "Host": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" }, "AllowedHosts": "*" } diff --git a/FictionArchive.Service.FileService/Models/IntegrationEvents/FileUploadRequestCreatedEvent.cs b/FictionArchive.Service.FileService/Models/IntegrationEvents/FileUploadRequestCreatedEvent.cs deleted file mode 100644 index bf03a56..0000000 --- a/FictionArchive.Service.FileService/Models/IntegrationEvents/FileUploadRequestCreatedEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.FileService.Models.IntegrationEvents; - -public class FileUploadRequestCreatedEvent : IIntegrationEvent -{ - public Guid RequestId { get; set; } - public string FilePath { get; set; } - public byte[] FileData { get; set; } -} \ No newline at end of file diff --git a/FictionArchive.Service.FileService/Models/IntegrationEvents/FileUploadRequestStatusUpdateEvent.cs b/FictionArchive.Service.FileService/Models/IntegrationEvents/FileUploadRequestStatusUpdateEvent.cs deleted file mode 100644 index b5936de..0000000 --- a/FictionArchive.Service.FileService/Models/IntegrationEvents/FileUploadRequestStatusUpdateEvent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FictionArchive.Common.Enums; -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.FileService.Models.IntegrationEvents; - -public class FileUploadRequestStatusUpdateEvent : IIntegrationEvent -{ - public Guid RequestId { get; set; } - public RequestStatus Status { get; set; } - - #region Success - - public string? FileAccessUrl { get; set; } - - #endregion - - #region Failure - - public string? ErrorMessage { get; set; } - - #endregion -} \ No newline at end of file diff --git a/FictionArchive.Service.FileService/Program.cs b/FictionArchive.Service.FileService/Program.cs index 7d8ac6d..dfb7d62 100644 --- a/FictionArchive.Service.FileService/Program.cs +++ b/FictionArchive.Service.FileService/Program.cs @@ -2,10 +2,9 @@ using Amazon.Runtime; using Amazon.S3; using FictionArchive.Common.Extensions; using FictionArchive.Service.FileService.Models; -using FictionArchive.Service.FileService.Models.IntegrationEvents; using FictionArchive.Service.FileService.Services.EventHandlers; using FictionArchive.Service.Shared.Extensions; -using FictionArchive.Service.Shared.Services.EventBus.Implementations; +using FictionArchive.Service.Shared.MassTransit; using Microsoft.Extensions.Options; namespace FictionArchive.Service.FileService; @@ -24,14 +23,15 @@ public class Program builder.Services.AddHealthChecks(); - #region Event Bus + #region MassTransit + + builder.Services.AddFictionArchiveMassTransit( + builder.Configuration, + x => + { + x.AddConsumer(); + }); - builder.Services.AddRabbitMQ(opt => - { - builder.Configuration.GetSection("RabbitMQ").Bind(opt); - }) - .Subscribe(); - #endregion // Add authentication with cookie support diff --git a/FictionArchive.Service.FileService/Services/EventHandlers/FileUploadRequestCreatedEventHandler.cs b/FictionArchive.Service.FileService/Services/EventHandlers/UploadFileCommandConsumer.cs similarity index 57% rename from FictionArchive.Service.FileService/Services/EventHandlers/FileUploadRequestCreatedEventHandler.cs rename to FictionArchive.Service.FileService/Services/EventHandlers/UploadFileCommandConsumer.cs index 0383030..f74adc2 100644 --- a/FictionArchive.Service.FileService/Services/EventHandlers/FileUploadRequestCreatedEventHandler.cs +++ b/FictionArchive.Service.FileService/Services/EventHandlers/UploadFileCommandConsumer.cs @@ -2,57 +2,62 @@ using Amazon.S3; using Amazon.S3.Model; using FictionArchive.Common.Enums; using FictionArchive.Service.FileService.Models; -using FictionArchive.Service.FileService.Models.IntegrationEvents; -using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.Shared.MassTransit.Contracts.Commands; +using FictionArchive.Service.Shared.MassTransit.Contracts.Events; +using MassTransit; using Microsoft.Extensions.Options; namespace FictionArchive.Service.FileService.Services.EventHandlers; -public class FileUploadRequestCreatedEventHandler : IIntegrationEventHandler +public class UploadFileCommandConsumer : IConsumer { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly AmazonS3Client _amazonS3Client; - private readonly IEventBus _eventBus; private readonly S3Configuration _s3Configuration; private readonly ProxyConfiguration _proxyConfiguration; - public FileUploadRequestCreatedEventHandler(ILogger logger, AmazonS3Client amazonS3Client, IEventBus eventBus, IOptions s3Configuration, IOptions proxyConfiguration) + public UploadFileCommandConsumer( + ILogger logger, + AmazonS3Client amazonS3Client, + IOptions s3Configuration, + IOptions proxyConfiguration) { _logger = logger; _amazonS3Client = amazonS3Client; - _eventBus = eventBus; _proxyConfiguration = proxyConfiguration.Value; _s3Configuration = s3Configuration.Value; } - public async Task Handle(FileUploadRequestCreatedEvent @event) + public async Task Consume(ConsumeContext context) { + var command = context.Message; + var putObjectRequest = new PutObjectRequest(); putObjectRequest.BucketName = _s3Configuration.Bucket; - putObjectRequest.Key = @event.FilePath; + putObjectRequest.Key = command.FilePath; putObjectRequest.UseChunkEncoding = false; // Needed to avoid an error with Garage - - using MemoryStream memoryStream = new MemoryStream(@event.FileData); + + using MemoryStream memoryStream = new MemoryStream(command.FileData); putObjectRequest.InputStream = memoryStream; var s3Response = await _amazonS3Client.PutObjectAsync(putObjectRequest); if (s3Response.HttpStatusCode != System.Net.HttpStatusCode.OK) { _logger.LogError("An error occurred while uploading file to S3. Response code: {responsecode}", s3Response.HttpStatusCode); - await _eventBus.Publish(new FileUploadRequestStatusUpdateEvent() + await context.Publish(new FileUploadCompletedEvent { - RequestId = @event.RequestId, + RequestId = command.RequestId, Status = RequestStatus.Failed, ErrorMessage = "An error occurred while uploading file to S3." }); return; } - await _eventBus.Publish(new FileUploadRequestStatusUpdateEvent() + await context.Publish(new FileUploadCompletedEvent { Status = RequestStatus.Success, - RequestId = @event.RequestId, - FileAccessUrl = _proxyConfiguration.BaseUrl + "/" + @event.FilePath + RequestId = command.RequestId, + FileAccessUrl = _proxyConfiguration.BaseUrl + "/" + command.FilePath }); } -} \ No newline at end of file +} diff --git a/FictionArchive.Service.FileService/appsettings.json b/FictionArchive.Service.FileService/appsettings.json index 0ec37b4..6e7f116 100644 --- a/FictionArchive.Service.FileService/appsettings.json +++ b/FictionArchive.Service.FileService/appsettings.json @@ -9,8 +9,10 @@ "BaseUrl": "https://localhost:7247/api" }, "RabbitMQ": { - "ConnectionString": "amqp://localhost", - "ClientIdentifier": "FileService" + "Host": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" }, "S3": { "Url": "https://s3.orfl.xyz", diff --git a/FictionArchive.Service.NovelService.Tests/NovelUpdateServiceTests.cs b/FictionArchive.Service.NovelService.Tests/NovelUpdateServiceTests.cs index d5d2790..0aef2f3 100644 --- a/FictionArchive.Service.NovelService.Tests/NovelUpdateServiceTests.cs +++ b/FictionArchive.Service.NovelService.Tests/NovelUpdateServiceTests.cs @@ -1,5 +1,4 @@ using FictionArchive.Common.Enums; -using FictionArchive.Service.FileService.IntegrationEvents; using FictionArchive.Service.NovelService.Models.Configuration; using FictionArchive.Service.NovelService.Models.Enums; using FictionArchive.Service.NovelService.Models.Images; @@ -8,8 +7,9 @@ using FictionArchive.Service.NovelService.Models.Novels; using FictionArchive.Service.NovelService.Models.SourceAdapters; using FictionArchive.Service.NovelService.Services; using FictionArchive.Service.NovelService.Services.SourceAdapters; -using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.Shared.MassTransit.Contracts.Commands; using FluentAssertions; +using MassTransit; using HtmlAgilityPack; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; @@ -72,7 +72,8 @@ public class NovelUpdateServiceTests private static NovelUpdateService CreateService( NovelServiceDbContext dbContext, ISourceAdapter adapter, - IEventBus eventBus, + IPublishEndpoint publishEndpoint, + ISendEndpointProvider sendEndpointProvider, string pendingImageUrl = "https://pending/placeholder.jpg") { var options = Options.Create(new NovelUpdateServiceConfiguration @@ -80,7 +81,7 @@ public class NovelUpdateServiceTests PendingImageUrl = pendingImageUrl }); - return new NovelUpdateService(dbContext, NullLogger.Instance, new[] { adapter }, eventBus, options); + return new NovelUpdateService(dbContext, NullLogger.Instance, new[] { adapter }, publishEndpoint, sendEndpointProvider, options); } [Fact] @@ -102,13 +103,15 @@ public class NovelUpdateServiceTests ImageData = new List { image1, image2 } })); - var publishedEvents = new List(); - var eventBus = Substitute.For(); - eventBus.Publish(Arg.Do(publishedEvents.Add)).Returns(Task.CompletedTask); - eventBus.Publish(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); + var publishedCommands = new List(); + var publishEndpoint = Substitute.For(); + var sendEndpointProvider = Substitute.For(); + var sendEndpoint = Substitute.For(); + sendEndpointProvider.GetSendEndpoint(Arg.Any()).Returns(Task.FromResult(sendEndpoint)); + sendEndpoint.Send(Arg.Do(publishedCommands.Add), Arg.Any()).Returns(Task.CompletedTask); var pendingImageUrl = "https://pending/placeholder.jpg"; - var service = CreateService(dbContext, adapter, eventBus, pendingImageUrl); + var service = CreateService(dbContext, adapter, publishEndpoint, sendEndpointProvider, pendingImageUrl); var updatedChapter = await service.PullChapterContents(novel.Id, volume.Id, chapter.Order); @@ -127,10 +130,10 @@ public class NovelUpdateServiceTests .Should() .BeEquivalentTo(updatedChapter.Images.Select(img => img.Id.ToString())); - publishedEvents.Should().HaveCount(2); - publishedEvents.Select(e => e.RequestId).Should().BeEquivalentTo(updatedChapter.Images.Select(i => i.Id)); - publishedEvents.Select(e => e.FileData).Should().BeEquivalentTo(new[] { image1.Data, image2.Data }); - publishedEvents.Should().OnlyContain(e => e.FilePath.StartsWith($"{novel.Id}/Images/Chapter-{updatedChapter.Id}/")); + publishedCommands.Should().HaveCount(2); + publishedCommands.Select(e => e.RequestId).Should().BeEquivalentTo(updatedChapter.Images.Select(i => i.Id)); + publishedCommands.Select(e => e.FileData).Should().BeEquivalentTo(new[] { image1.Data, image2.Data }); + publishedCommands.Should().OnlyContain(e => e.FilePath.StartsWith($"{novel.Id}/Images/Chapter-{updatedChapter.Id}/")); } [Fact] @@ -151,11 +154,12 @@ public class NovelUpdateServiceTests ImageData = new List { image } })); - var eventBus = Substitute.For(); - eventBus.Publish(Arg.Any()).Returns(Task.CompletedTask); - eventBus.Publish(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); + var publishEndpoint = Substitute.For(); + var sendEndpointProvider = Substitute.For(); + var sendEndpoint = Substitute.For(); + sendEndpointProvider.GetSendEndpoint(Arg.Any()).Returns(Task.FromResult(sendEndpoint)); - var service = CreateService(dbContext, adapter, eventBus); + var service = CreateService(dbContext, adapter, publishEndpoint, sendEndpointProvider); var updatedChapter = await service.PullChapterContents(novel.Id, volume.Id, chapter.Order); @@ -186,8 +190,9 @@ public class NovelUpdateServiceTests await dbContext.SaveChangesAsync(); var adapter = Substitute.For(); - var eventBus = Substitute.For(); - var service = CreateService(dbContext, adapter, eventBus); + var publishEndpoint = Substitute.For(); + var sendEndpointProvider = Substitute.For(); + var service = CreateService(dbContext, adapter, publishEndpoint, sendEndpointProvider); var newUrl = "https://cdn.example.com/uploaded/cover.jpg"; @@ -228,8 +233,9 @@ public class NovelUpdateServiceTests await dbContext.SaveChangesAsync(); var adapter = Substitute.For(); - var eventBus = Substitute.For(); - var service = CreateService(dbContext, adapter, eventBus, pendingUrl); + var publishEndpoint = Substitute.For(); + var sendEndpointProvider = Substitute.For(); + var service = CreateService(dbContext, adapter, publishEndpoint, sendEndpointProvider, pendingUrl); var newUrl = "https://cdn.example.com/uploaded/image.jpg"; @@ -277,8 +283,9 @@ public class NovelUpdateServiceTests await dbContext.SaveChangesAsync(); var adapter = Substitute.For(); - var eventBus = Substitute.For(); - var service = CreateService(dbContext, adapter, eventBus, pendingUrl); + var publishEndpoint = Substitute.For(); + var sendEndpointProvider = Substitute.For(); + var service = CreateService(dbContext, adapter, publishEndpoint, sendEndpointProvider, pendingUrl); var newUrl = "https://cdn.example.com/uploaded/img1.jpg"; diff --git a/FictionArchive.Service.NovelService/GraphQL/Mutation.cs b/FictionArchive.Service.NovelService/GraphQL/Mutation.cs index 47c3e40..348523b 100644 --- a/FictionArchive.Service.NovelService/GraphQL/Mutation.cs +++ b/FictionArchive.Service.NovelService/GraphQL/Mutation.cs @@ -1,27 +1,20 @@ -using FictionArchive.Service.NovelService.Models.Enums; -using FictionArchive.Service.NovelService.Models.IntegrationEvents; -using FictionArchive.Service.NovelService.Models.Localization; -using FictionArchive.Service.NovelService.Models.Novels; -using FictionArchive.Service.NovelService.Models.SourceAdapters; using FictionArchive.Service.NovelService.Services; -using FictionArchive.Service.NovelService.Services.SourceAdapters; -using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.Shared.MassTransit.Contracts.Commands; using HotChocolate.Authorization; using HotChocolate.Types; -using Microsoft.EntityFrameworkCore; namespace FictionArchive.Service.NovelService.GraphQL; public class Mutation { [Authorize] - public async Task ImportNovel(string novelUrl, NovelUpdateService service) + public async Task ImportNovel(string novelUrl, NovelUpdateService service) { return await service.QueueNovelImport(novelUrl); } [Authorize] - public async Task FetchChapterContents( + public async Task FetchChapterContents( uint novelId, uint volumeId, uint chapterOrder, diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/ChapterPullRequestedEvent.cs b/FictionArchive.Service.NovelService/Models/IntegrationEvents/ChapterPullRequestedEvent.cs deleted file mode 100644 index cd70549..0000000 --- a/FictionArchive.Service.NovelService/Models/IntegrationEvents/ChapterPullRequestedEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; - -public class ChapterPullRequestedEvent : IIntegrationEvent -{ - public uint NovelId { get; set; } - public uint VolumeId { get; set; } - public uint ChapterOrder { get; set; } -} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/FileUploadRequestCreatedEvent.cs b/FictionArchive.Service.NovelService/Models/IntegrationEvents/FileUploadRequestCreatedEvent.cs deleted file mode 100644 index 44e3f46..0000000 --- a/FictionArchive.Service.NovelService/Models/IntegrationEvents/FileUploadRequestCreatedEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.FileService.IntegrationEvents; - -public class FileUploadRequestCreatedEvent : IIntegrationEvent -{ - public Guid RequestId { get; set; } - public string FilePath { get; set; } - public byte[] FileData { get; set; } -} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/FileUploadRequestStatusUpdateEvent.cs b/FictionArchive.Service.NovelService/Models/IntegrationEvents/FileUploadRequestStatusUpdateEvent.cs deleted file mode 100644 index 4f61613..0000000 --- a/FictionArchive.Service.NovelService/Models/IntegrationEvents/FileUploadRequestStatusUpdateEvent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FictionArchive.Common.Enums; -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; - -public class FileUploadRequestStatusUpdateEvent : IIntegrationEvent -{ - public Guid RequestId { get; set; } - public RequestStatus Status { get; set; } - - #region Success - - public string? FileAccessUrl { get; set; } - - #endregion - - #region Failure - - public string? ErrorMessage { get; set; } - - #endregion -} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/NovelUpdateRequestedEvent.cs b/FictionArchive.Service.NovelService/Models/IntegrationEvents/NovelUpdateRequestedEvent.cs deleted file mode 100644 index 5243a3f..0000000 --- a/FictionArchive.Service.NovelService/Models/IntegrationEvents/NovelUpdateRequestedEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; - -public class NovelUpdateRequestedEvent : IIntegrationEvent -{ - public string NovelUrl { get; set; } -} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs b/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs deleted file mode 100644 index 98d5ee6..0000000 --- a/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using FictionArchive.Common.Enums; -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; - -public class TranslationRequestCompletedEvent : IIntegrationEvent -{ - /// - /// Maps this event back to a triggering request. - /// - public Guid? TranslationRequestId { get; set; } - - /// - /// The resulting text. - /// - public string? TranslatedText { get; set; } -} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs b/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs deleted file mode 100644 index e6623f0..0000000 --- a/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FictionArchive.Common.Enums; -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; - -public class TranslationRequestCreatedEvent : IIntegrationEvent -{ - public Guid TranslationRequestId { get; set; } - public Language From { get; set; } - public Language To { get; set; } - public string Body { get; set; } - public string TranslationEngineKey { get; set; } -} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Program.cs b/FictionArchive.Service.NovelService/Program.cs index e06f9b9..1c21b48 100644 --- a/FictionArchive.Service.NovelService/Program.cs +++ b/FictionArchive.Service.NovelService/Program.cs @@ -1,14 +1,13 @@ using FictionArchive.Common.Extensions; using FictionArchive.Service.NovelService.GraphQL; using FictionArchive.Service.NovelService.Models.Configuration; -using FictionArchive.Service.NovelService.Models.IntegrationEvents; using FictionArchive.Service.NovelService.Services; -using FictionArchive.Service.NovelService.Services.EventHandlers; +using FictionArchive.Service.NovelService.Services.Consumers; using FictionArchive.Service.NovelService.Services.SourceAdapters; using FictionArchive.Service.NovelService.Services.SourceAdapters.Novelpia; using FictionArchive.Service.Shared; using FictionArchive.Service.Shared.Extensions; -using FictionArchive.Service.Shared.Services.EventBus.Implementations; +using FictionArchive.Service.Shared.MassTransit; using FictionArchive.Service.Shared.Services.GraphQL; using Microsoft.EntityFrameworkCore; @@ -25,18 +24,19 @@ public class Program builder.Services.AddMemoryCache(); - #region Event Bus + #region MassTransit if (!isSchemaExport) { - builder.Services.AddRabbitMQ(opt => - { - builder.Configuration.GetSection("RabbitMQ").Bind(opt); - }) - .Subscribe() - .Subscribe() - .Subscribe() - .Subscribe(); + builder.Services.AddFictionArchiveMassTransit( + builder.Configuration, + cfg => + { + cfg.AddConsumer(); + cfg.AddConsumer(); + cfg.AddConsumer(); + cfg.AddConsumer(); + }); } #endregion diff --git a/FictionArchive.Service.NovelService/Services/Consumers/FileUploadCompletedEventConsumer.cs b/FictionArchive.Service.NovelService/Services/Consumers/FileUploadCompletedEventConsumer.cs new file mode 100644 index 0000000..266261f --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/Consumers/FileUploadCompletedEventConsumer.cs @@ -0,0 +1,53 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.Shared.MassTransit.Contracts.Events; +using MassTransit; + +namespace FictionArchive.Service.NovelService.Services.Consumers; + +public class FileUploadCompletedEventConsumer : IConsumer +{ + private readonly ILogger _logger; + private readonly NovelServiceDbContext _dbContext; + private readonly NovelUpdateService _novelUpdateService; + + public FileUploadCompletedEventConsumer( + ILogger logger, + NovelServiceDbContext dbContext, + NovelUpdateService novelUpdateService) + { + _logger = logger; + _dbContext = dbContext; + _novelUpdateService = novelUpdateService; + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + + var image = await _dbContext.Images.FindAsync(@event.RequestId); + if (image == null) + { + // Not a request we care about. + _logger.LogDebug( + "FileUploadCompletedEvent received for unknown image: {RequestId}", + @event.RequestId); + return; + } + + if (@event.Status == RequestStatus.Failed) + { + _logger.LogError( + "Image upload failed for image with id {ImageId}: {ErrorMessage}", + image.Id, @event.ErrorMessage); + return; + } + + if (@event.Status == RequestStatus.Success) + { + _logger.LogInformation( + "Image upload succeeded for image with id {ImageId}", + image.Id); + await _novelUpdateService.UpdateImage(image.Id, @event.FileAccessUrl!); + } + } +} diff --git a/FictionArchive.Service.NovelService/Services/Consumers/ImportNovelCommandConsumer.cs b/FictionArchive.Service.NovelService/Services/Consumers/ImportNovelCommandConsumer.cs new file mode 100644 index 0000000..ef2846f --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/Consumers/ImportNovelCommandConsumer.cs @@ -0,0 +1,26 @@ +using FictionArchive.Service.Shared.MassTransit.Contracts.Commands; +using MassTransit; + +namespace FictionArchive.Service.NovelService.Services.Consumers; + +public class ImportNovelCommandConsumer : IConsumer +{ + private readonly ILogger _logger; + private readonly NovelUpdateService _novelUpdateService; + + public ImportNovelCommandConsumer( + ILogger logger, + NovelUpdateService novelUpdateService) + { + _logger = logger; + _novelUpdateService = novelUpdateService; + } + + public async Task Consume(ConsumeContext context) + { + var command = context.Message; + _logger.LogInformation("Processing ImportNovelCommand for URL: {NovelUrl}", command.NovelUrl); + + await _novelUpdateService.ImportNovel(command.NovelUrl); + } +} diff --git a/FictionArchive.Service.NovelService/Services/Consumers/PullChapterContentCommandConsumer.cs b/FictionArchive.Service.NovelService/Services/Consumers/PullChapterContentCommandConsumer.cs new file mode 100644 index 0000000..fd79064 --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/Consumers/PullChapterContentCommandConsumer.cs @@ -0,0 +1,28 @@ +using FictionArchive.Service.Shared.MassTransit.Contracts.Commands; +using MassTransit; + +namespace FictionArchive.Service.NovelService.Services.Consumers; + +public class PullChapterContentCommandConsumer : IConsumer +{ + private readonly ILogger _logger; + private readonly NovelUpdateService _novelUpdateService; + + public PullChapterContentCommandConsumer( + ILogger logger, + NovelUpdateService novelUpdateService) + { + _logger = logger; + _novelUpdateService = novelUpdateService; + } + + public async Task Consume(ConsumeContext context) + { + var command = context.Message; + _logger.LogInformation( + "Processing PullChapterContentCommand for Novel: {NovelId}, Volume: {VolumeId}, Chapter: {ChapterOrder}", + command.NovelId, command.VolumeId, command.ChapterOrder); + + await _novelUpdateService.PullChapterContents(command.NovelId, command.VolumeId, command.ChapterOrder); + } +} diff --git a/FictionArchive.Service.NovelService/Services/Consumers/TranslationCompletedEventConsumer.cs b/FictionArchive.Service.NovelService/Services/Consumers/TranslationCompletedEventConsumer.cs new file mode 100644 index 0000000..aa1659b --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/Consumers/TranslationCompletedEventConsumer.cs @@ -0,0 +1,52 @@ +using FictionArchive.Service.NovelService.Models.Localization; +using FictionArchive.Service.Shared.MassTransit.Contracts.Events; +using MassTransit; +using Microsoft.EntityFrameworkCore; + +namespace FictionArchive.Service.NovelService.Services.Consumers; + +public class TranslationCompletedEventConsumer : IConsumer +{ + private readonly ILogger _logger; + private readonly NovelServiceDbContext _dbContext; + + public TranslationCompletedEventConsumer( + ILogger logger, + NovelServiceDbContext dbContext) + { + _logger = logger; + _dbContext = dbContext; + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + + var localizationRequest = await _dbContext.LocalizationRequests + .Include(r => r.KeyRequestedForTranslation) + .ThenInclude(lk => lk.Texts) + .FirstOrDefaultAsync(lk => lk.Id == @event.TranslationRequestId); + + if (localizationRequest == null) + { + // Not one of our requests, discard it + _logger.LogDebug( + "TranslationCompletedEvent received for unknown request: {RequestId}", + @event.TranslationRequestId); + return; + } + + localizationRequest.KeyRequestedForTranslation.Texts.Add(new LocalizationText() + { + Language = localizationRequest.TranslateTo, + Text = @event.TranslatedText, + TranslationEngine = localizationRequest.Engine + }); + _dbContext.LocalizationRequests.Remove(localizationRequest); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "Completed translation for request: {RequestId}", + @event.TranslationRequestId); + } +} diff --git a/FictionArchive.Service.NovelService/Services/EventHandlers/ChapterPullRequestedEventHandler.cs b/FictionArchive.Service.NovelService/Services/EventHandlers/ChapterPullRequestedEventHandler.cs deleted file mode 100644 index 6acd8ee..0000000 --- a/FictionArchive.Service.NovelService/Services/EventHandlers/ChapterPullRequestedEventHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FictionArchive.Service.NovelService.Models.IntegrationEvents; -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.NovelService.Services.EventHandlers; - -public class ChapterPullRequestedEventHandler : IIntegrationEventHandler -{ - private readonly NovelUpdateService _novelUpdateService; - - public ChapterPullRequestedEventHandler(NovelUpdateService novelUpdateService) - { - _novelUpdateService = novelUpdateService; - } - - public async Task Handle(ChapterPullRequestedEvent @event) - { - await _novelUpdateService.PullChapterContents(@event.NovelId, @event.VolumeId, @event.ChapterOrder); - } -} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/EventHandlers/FileUploadRequestStatusUpdateEventHandler.cs b/FictionArchive.Service.NovelService/Services/EventHandlers/FileUploadRequestStatusUpdateEventHandler.cs deleted file mode 100644 index f6b3dc3..0000000 --- a/FictionArchive.Service.NovelService/Services/EventHandlers/FileUploadRequestStatusUpdateEventHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using FictionArchive.Common.Enums; -using FictionArchive.Service.NovelService.Models.IntegrationEvents; -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.NovelService.Services.EventHandlers; - -public class FileUploadRequestStatusUpdateEventHandler : IIntegrationEventHandler -{ - private readonly ILogger _logger; - private readonly NovelServiceDbContext _context; - private readonly NovelUpdateService _novelUpdateService; - - public FileUploadRequestStatusUpdateEventHandler(ILogger logger, NovelServiceDbContext context, NovelUpdateService novelUpdateService) - { - _logger = logger; - _context = context; - _novelUpdateService = novelUpdateService; - } - - public async Task Handle(FileUploadRequestStatusUpdateEvent @event) - { - var image = await _context.Images.FindAsync(@event.RequestId); - if (image == null) - { - // Not a request we care about. - return; - } - if (@event.Status == RequestStatus.Failed) - { - _logger.LogError("Image upload failed for image with id {imageId}", image.Id); - return; - } - else if (@event.Status == RequestStatus.Success) - { - _logger.LogInformation("Image upload succeeded for image with id {imageId}", image.Id); - await _novelUpdateService.UpdateImage(image.Id, @event.FileAccessUrl); - } - } -} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/EventHandlers/NovelUpdateRequestedEventHandler.cs b/FictionArchive.Service.NovelService/Services/EventHandlers/NovelUpdateRequestedEventHandler.cs deleted file mode 100644 index 5fad1e0..0000000 --- a/FictionArchive.Service.NovelService/Services/EventHandlers/NovelUpdateRequestedEventHandler.cs +++ /dev/null @@ -1,23 +0,0 @@ -using FictionArchive.Service.NovelService.Models.IntegrationEvents; -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.NovelService.Services.EventHandlers; - -public class NovelUpdateRequestedEventHandler : IIntegrationEventHandler -{ - private readonly ILogger _logger; - private readonly IEventBus _eventBus; - private readonly NovelUpdateService _novelUpdateService; - - public NovelUpdateRequestedEventHandler(ILogger logger, IEventBus eventBus, NovelUpdateService novelUpdateService) - { - _logger = logger; - _eventBus = eventBus; - _novelUpdateService = novelUpdateService; - } - - public async Task Handle(NovelUpdateRequestedEvent @event) - { - await _novelUpdateService.ImportNovel(@event.NovelUrl); - } -} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/EventHandlers/TranslationRequestCompletedEventHandler.cs b/FictionArchive.Service.NovelService/Services/EventHandlers/TranslationRequestCompletedEventHandler.cs deleted file mode 100644 index 632d26c..0000000 --- a/FictionArchive.Service.NovelService/Services/EventHandlers/TranslationRequestCompletedEventHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using FictionArchive.Service.NovelService.Models.IntegrationEvents; -using FictionArchive.Service.NovelService.Models.Localization; -using FictionArchive.Service.Shared.Services.EventBus; -using Microsoft.EntityFrameworkCore; - -namespace FictionArchive.Service.NovelService.Services.EventHandlers; - -public class TranslationRequestCompletedEventHandler : IIntegrationEventHandler -{ - private readonly ILogger _logger; - private readonly NovelServiceDbContext _dbContext; - - public TranslationRequestCompletedEventHandler(ILogger logger, NovelServiceDbContext dbContext) - { - _logger = logger; - _dbContext = dbContext; - } - - public async Task Handle(TranslationRequestCompletedEvent @event) - { - var localizationRequest = await _dbContext.LocalizationRequests.Include(r => r.KeyRequestedForTranslation) - .ThenInclude(lk => lk.Texts) - .FirstOrDefaultAsync(lk => lk.Id == @event.TranslationRequestId); - if (localizationRequest == null) - { - // Not one of our requests, discard it - return; - } - - localizationRequest.KeyRequestedForTranslation.Texts.Add(new LocalizationText() - { - Language = localizationRequest.TranslateTo, - Text = @event.TranslatedText, - TranslationEngine = localizationRequest.Engine - }); - _dbContext.LocalizationRequests.Remove(localizationRequest); - await _dbContext.SaveChangesAsync(); - } -} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/NovelUpdateService.cs b/FictionArchive.Service.NovelService/Services/NovelUpdateService.cs index d95e865..dd3e98e 100644 --- a/FictionArchive.Service.NovelService/Services/NovelUpdateService.cs +++ b/FictionArchive.Service.NovelService/Services/NovelUpdateService.cs @@ -1,15 +1,15 @@ using FictionArchive.Common.Enums; -using FictionArchive.Service.FileService.IntegrationEvents; using FictionArchive.Service.NovelService.Models.Configuration; using FictionArchive.Service.NovelService.Models.Enums; using FictionArchive.Service.NovelService.Models.Images; -using FictionArchive.Service.NovelService.Models.IntegrationEvents; using FictionArchive.Service.NovelService.Models.Localization; using FictionArchive.Service.NovelService.Models.Novels; using FictionArchive.Service.NovelService.Models.SourceAdapters; using FictionArchive.Service.NovelService.Services.SourceAdapters; -using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.Shared.MassTransit.Contracts.Commands; +using FictionArchive.Service.Shared.MassTransit.Contracts.Events; using HtmlAgilityPack; +using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -20,15 +20,23 @@ public class NovelUpdateService private readonly NovelServiceDbContext _dbContext; private readonly ILogger _logger; private readonly IEnumerable _sourceAdapters; - private readonly IEventBus _eventBus; + private readonly IPublishEndpoint _publishEndpoint; + private readonly ISendEndpointProvider _sendEndpointProvider; private readonly NovelUpdateServiceConfiguration _novelUpdateServiceConfiguration; - public NovelUpdateService(NovelServiceDbContext dbContext, ILogger logger, IEnumerable sourceAdapters, IEventBus eventBus, IOptions novelUpdateServiceConfiguration) + public NovelUpdateService( + NovelServiceDbContext dbContext, + ILogger logger, + IEnumerable sourceAdapters, + IPublishEndpoint publishEndpoint, + ISendEndpointProvider sendEndpointProvider, + IOptions novelUpdateServiceConfiguration) { _dbContext = dbContext; _logger = logger; _sourceAdapters = sourceAdapters; - _eventBus = eventBus; + _publishEndpoint = publishEndpoint; + _sendEndpointProvider = sendEndpointProvider; _novelUpdateServiceConfiguration = novelUpdateServiceConfiguration.Value; } @@ -393,7 +401,7 @@ public class NovelUpdateService // Publish novel created event for new novels if (existingNovel == null) { - await _eventBus.Publish(new NovelCreatedEvent + await _publishEndpoint.Publish(new NovelCreatedEvent { NovelId = novel.Id, Title = novel.Name.Texts.First(t => t.Language == novel.RawLanguage).Text, @@ -408,7 +416,7 @@ public class NovelUpdateService { foreach (var chapter in volume.Chapters.Where(c => !existingChapterIds.Contains(c.Id))) { - await _eventBus.Publish(new ChapterCreatedEvent + await _publishEndpoint.Publish(new ChapterCreatedEvent { ChapterId = chapter.Id, NovelId = novel.Id, @@ -420,10 +428,11 @@ public class NovelUpdateService } } - // Publish cover image event if needed + // Send cover image upload command if needed if (shouldPublishCoverEvent && novel.CoverImage != null && metadata.CoverImage != null) { - await _eventBus.Publish(new FileUploadRequestCreatedEvent + var uploadEndpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri("queue:upload-file-command")); + await uploadEndpoint.Send(new UploadFileCommand { RequestId = novel.CoverImage.Id, FileData = metadata.CoverImage.Data, @@ -431,7 +440,8 @@ public class NovelUpdateService }); } - // Publish chapter pull events for chapters without body content + // Send chapter pull commands for chapters without body content + var pullChapterEndpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri("queue:pull-chapter-content-command")); foreach (var volume in novel.Volumes) { var chaptersNeedingPull = volume.Chapters @@ -440,7 +450,7 @@ public class NovelUpdateService foreach (var chapter in chaptersNeedingPull) { - await _eventBus.Publish(new ChapterPullRequestedEvent + await pullChapterEndpoint.Send(new PullChapterContentCommand { NovelId = novel.Id, VolumeId = volume.Id, @@ -513,12 +523,13 @@ public class NovelUpdateService localizationText.Text = chapterDoc.DocumentNode.OuterHtml; await _dbContext.SaveChangesAsync(); - // Body was updated, raise image request + // Body was updated, send upload commands for images + var uploadEndpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri("queue:upload-file-command")); int imgCount = 0; foreach (var image in chapter.Images) { var data = rawChapter.ImageData.FirstOrDefault(img => img.Url == image.OriginalPath); - await _eventBus.Publish(new FileUploadRequestCreatedEvent() + await uploadEndpoint.Send(new UploadFileCommand { FileData = data.Data, FilePath = $"{novel.Id}/Images/Chapter-{chapter.Id}/{imgCount++}.jpg", @@ -557,26 +568,28 @@ public class NovelUpdateService await _dbContext.SaveChangesAsync(); } - public async Task QueueNovelImport(string novelUrl) + public async Task QueueNovelImport(string novelUrl) { - var importNovelRequestEvent = new NovelUpdateRequestedEvent() + var command = new ImportNovelCommand { NovelUrl = novelUrl }; - await _eventBus.Publish(importNovelRequestEvent); - return importNovelRequestEvent; + var endpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri("queue:import-novel-command")); + await endpoint.Send(command); + return command; } - public async Task QueueChapterPull(uint novelId, uint volumeId, uint chapterOrder) + public async Task QueueChapterPull(uint novelId, uint volumeId, uint chapterOrder) { - var chapterPullEvent = new ChapterPullRequestedEvent() + var command = new PullChapterContentCommand { NovelId = novelId, VolumeId = volumeId, ChapterOrder = chapterOrder }; - await _eventBus.Publish(chapterPullEvent); - return chapterPullEvent; + var endpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri("queue:pull-chapter-content-command")); + await endpoint.Send(command); + return command; } public async Task DeleteNovel(uint novelId) diff --git a/FictionArchive.Service.NovelService/appsettings.json b/FictionArchive.Service.NovelService/appsettings.json index f287976..ad88bf3 100644 --- a/FictionArchive.Service.NovelService/appsettings.json +++ b/FictionArchive.Service.NovelService/appsettings.json @@ -16,8 +16,10 @@ "DefaultConnection": "Host=localhost;Database=FictionArchive_NovelService;Username=postgres;password=postgres" }, "RabbitMQ": { - "ConnectionString": "amqp://localhost", - "ClientIdentifier": "NovelService" + "Host": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" }, "AllowedHosts": "*", "OIDC": { diff --git a/FictionArchive.Service.ReportingService/Dockerfile b/FictionArchive.Service.ReportingService/Dockerfile new file mode 100644 index 0000000..a8a453b --- /dev/null +++ b/FictionArchive.Service.ReportingService/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["FictionArchive.Service.ReportingService/FictionArchive.Service.ReportingService.csproj", "FictionArchive.Service.ReportingService/"] +RUN dotnet restore "FictionArchive.Service.ReportingService/FictionArchive.Service.ReportingService.csproj" +COPY . . +WORKDIR "/src/FictionArchive.Service.ReportingService" +RUN dotnet build "./FictionArchive.Service.ReportingService.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./FictionArchive.Service.ReportingService.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "FictionArchive.Service.ReportingService.dll"] diff --git a/FictionArchive.Service.ReportingService/FictionArchive.Service.ReportingService.csproj b/FictionArchive.Service.ReportingService/FictionArchive.Service.ReportingService.csproj new file mode 100644 index 0000000..9b2b66a --- /dev/null +++ b/FictionArchive.Service.ReportingService/FictionArchive.Service.ReportingService.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/FictionArchive.Service.ReportingService/GraphQL/Mutation.cs b/FictionArchive.Service.ReportingService/GraphQL/Mutation.cs new file mode 100644 index 0000000..4565473 --- /dev/null +++ b/FictionArchive.Service.ReportingService/GraphQL/Mutation.cs @@ -0,0 +1,15 @@ +using HotChocolate; +using HotChocolate.Authorization; + +namespace FictionArchive.Service.ReportingService.GraphQL; + +public class Mutation +{ + /// + /// Placeholder mutation for GraphQL schema requirements. + /// The ReportingService is primarily read-only, consuming events from other services. + /// + [Authorize(Roles = ["admin"])] + [GraphQLDescription("Placeholder mutation. ReportingService is primarily read-only.")] + public bool Ping() => true; +} diff --git a/FictionArchive.Service.ReportingService/GraphQL/Query.cs b/FictionArchive.Service.ReportingService/GraphQL/Query.cs new file mode 100644 index 0000000..4ae6d4b --- /dev/null +++ b/FictionArchive.Service.ReportingService/GraphQL/Query.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using FictionArchive.Service.ReportingService.Models.Database; +using FictionArchive.Service.ReportingService.Models.DTOs; +using FictionArchive.Service.ReportingService.Services; +using HotChocolate; +using HotChocolate.Data; +using Microsoft.EntityFrameworkCore; + +namespace FictionArchive.Service.ReportingService.GraphQL; + +public class Query +{ + [UseProjection] + [UseFiltering] + [UseSorting] + [GraphQLName("reportingJobs")] + public IQueryable GetReportingJobs(ReportingServiceDbContext dbContext) + => dbContext.Jobs.Include(j => j.History); + + [GraphQLName("reportingJob")] + public async Task GetReportingJob(Guid id, ReportingServiceDbContext dbContext) + { + var job = await dbContext.Jobs + .Include(j => j.History.OrderBy(h => h.Timestamp)) + .FirstOrDefaultAsync(j => j.Id == id); + + if (job == null) return null; + + return new JobDto + { + Id = job.Id, + JobType = job.JobType, + Status = job.Status, + CurrentStep = job.CurrentStep, + ErrorMessage = job.ErrorMessage, + Metadata = job.Metadata != null + ? JsonSerializer.Deserialize>(job.Metadata.RootElement.GetRawText()) + : null, + History = job.History.Select(h => new JobHistoryEntryDto + { + FromState = h.FromState, + ToState = h.ToState, + Message = h.Message, + Error = h.Error, + Timestamp = h.Timestamp + }).ToList(), + CreatedTime = job.CreatedTime, + UpdatedTime = job.UpdatedTime, + CompletedTime = job.CompletedTime + }; + } +} diff --git a/FictionArchive.Service.ReportingService/Models/DTOs/JobDto.cs b/FictionArchive.Service.ReportingService/Models/DTOs/JobDto.cs new file mode 100644 index 0000000..4a88e59 --- /dev/null +++ b/FictionArchive.Service.ReportingService/Models/DTOs/JobDto.cs @@ -0,0 +1,17 @@ +using NodaTime; + +namespace FictionArchive.Service.ReportingService.Models.DTOs; + +public class JobDto +{ + public Guid Id { get; set; } + public required string JobType { get; set; } + public required string Status { get; set; } + public string? CurrentStep { get; set; } + public string? ErrorMessage { get; set; } + public Dictionary? Metadata { get; set; } + public List History { get; set; } = new(); + public Instant CreatedTime { get; set; } + public Instant UpdatedTime { get; set; } + public Instant? CompletedTime { get; set; } +} diff --git a/FictionArchive.Service.ReportingService/Models/DTOs/JobHistoryEntryDto.cs b/FictionArchive.Service.ReportingService/Models/DTOs/JobHistoryEntryDto.cs new file mode 100644 index 0000000..4cb94fd --- /dev/null +++ b/FictionArchive.Service.ReportingService/Models/DTOs/JobHistoryEntryDto.cs @@ -0,0 +1,12 @@ +using NodaTime; + +namespace FictionArchive.Service.ReportingService.Models.DTOs; + +public class JobHistoryEntryDto +{ + public required string FromState { get; set; } + public required string ToState { get; set; } + public string? Message { get; set; } + public string? Error { get; set; } + public Instant Timestamp { get; set; } +} diff --git a/FictionArchive.Service.ReportingService/Models/Database/Job.cs b/FictionArchive.Service.ReportingService/Models/Database/Job.cs new file mode 100644 index 0000000..9654861 --- /dev/null +++ b/FictionArchive.Service.ReportingService/Models/Database/Job.cs @@ -0,0 +1,19 @@ +using System.Text.Json; +using NodaTime; + +namespace FictionArchive.Service.ReportingService.Models.Database; + +public class Job +{ + public Guid Id { get; set; } + public required string JobType { get; set; } + public required string Status { get; set; } + public string? CurrentStep { get; set; } + public string? ErrorMessage { get; set; } + public JsonDocument? Metadata { get; set; } + public Instant CreatedTime { get; set; } + public Instant UpdatedTime { get; set; } + public Instant? CompletedTime { get; set; } + + public ICollection History { get; set; } = new List(); +} diff --git a/FictionArchive.Service.ReportingService/Models/Database/JobHistoryEntry.cs b/FictionArchive.Service.ReportingService/Models/Database/JobHistoryEntry.cs new file mode 100644 index 0000000..2e3b934 --- /dev/null +++ b/FictionArchive.Service.ReportingService/Models/Database/JobHistoryEntry.cs @@ -0,0 +1,16 @@ +using NodaTime; + +namespace FictionArchive.Service.ReportingService.Models.Database; + +public class JobHistoryEntry +{ + public int Id { get; set; } + public Guid JobId { get; set; } + public required string FromState { get; set; } + public required string ToState { get; set; } + public string? Message { get; set; } + public string? Error { get; set; } + public Instant Timestamp { get; set; } + + public Job Job { get; set; } = null!; +} diff --git a/FictionArchive.Service.ReportingService/Program.cs b/FictionArchive.Service.ReportingService/Program.cs new file mode 100644 index 0000000..b9e9dc4 --- /dev/null +++ b/FictionArchive.Service.ReportingService/Program.cs @@ -0,0 +1,76 @@ +using FictionArchive.Common.Extensions; +using FictionArchive.Service.ReportingService.GraphQL; +using FictionArchive.Service.ReportingService.Services; +using FictionArchive.Service.ReportingService.Services.Consumers; +using FictionArchive.Service.Shared; +using FictionArchive.Service.Shared.Extensions; +using FictionArchive.Service.Shared.MassTransit; + +namespace FictionArchive.Service.ReportingService; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + var isSchemaExport = SchemaExportDetector.IsSchemaExportMode(args); + + builder.AddLocalAppsettings(); + + builder.Services.AddHealthChecks(); + + #region Database + + builder.Services.RegisterDbContext( + builder.Configuration.GetConnectionString("DefaultConnection")!, + skipInfrastructure: isSchemaExport); + + #endregion + + #region MassTransit + + if (!isSchemaExport) + { + builder.Services.AddFictionArchiveMassTransit( + builder.Configuration, + x => + { + x.AddConsumer(); + }); + } + + #endregion + + #region GraphQL + + builder.Services.AddDefaultGraphQl() + .AddAuthorization(); + + #endregion + + // Authentication & Authorization + builder.Services.AddOidcAuthentication(builder.Configuration); + builder.Services.AddFictionArchiveAuthorization(); + + var app = builder.Build(); + + if (!isSchemaExport) + { + using var scope = app.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.UpdateDatabase(); + } + + app.UseHttpsRedirection(); + + app.MapHealthChecks("/healthz"); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapGraphQL(); + + app.RunWithGraphQLCommands(args); + } +} diff --git a/FictionArchive.Service.ReportingService/Properties/launchSettings.json b/FictionArchive.Service.ReportingService/Properties/launchSettings.json new file mode 100644 index 0000000..8ca3bfa --- /dev/null +++ b/FictionArchive.Service.ReportingService/Properties/launchSettings.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:45320", + "sslPort": 44320 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5180", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "graphql", + "applicationUrl": "https://localhost:7320;http://localhost:5180", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/FictionArchive.Service.ReportingService/Services/Consumers/JobStateChangedEventConsumer.cs b/FictionArchive.Service.ReportingService/Services/Consumers/JobStateChangedEventConsumer.cs new file mode 100644 index 0000000..ee7e7e7 --- /dev/null +++ b/FictionArchive.Service.ReportingService/Services/Consumers/JobStateChangedEventConsumer.cs @@ -0,0 +1,74 @@ +using System.Text.Json; +using FictionArchive.Service.ReportingService.Models.Database; +using FictionArchive.Service.Shared.MassTransit.Contracts; +using MassTransit; + +namespace FictionArchive.Service.ReportingService.Services.Consumers; + +public class JobStateChangedEventConsumer : IConsumer +{ + private readonly ReportingServiceDbContext _dbContext; + private readonly ILogger _logger; + + public JobStateChangedEventConsumer( + ReportingServiceDbContext dbContext, + ILogger logger) + { + _dbContext = dbContext; + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + + var job = await _dbContext.Jobs.FindAsync(@event.JobId); + + if (job == null) + { + job = new Job + { + Id = @event.JobId, + JobType = @event.JobType, + Status = @event.ToState, + CreatedTime = @event.Timestamp, + UpdatedTime = @event.Timestamp, + Metadata = @event.Metadata != null + ? JsonSerializer.SerializeToDocument(@event.Metadata) + : null + }; + _dbContext.Jobs.Add(job); + } + else + { + job.Status = @event.ToState; + job.UpdatedTime = @event.Timestamp; + + if (@event.Error != null) + { + job.ErrorMessage = @event.Error; + } + + if (@event.ToState is "Completed" or "Failed") + { + job.CompletedTime = @event.Timestamp; + } + } + + var historyEntry = new JobHistoryEntry + { + JobId = @event.JobId, + FromState = @event.FromState, + ToState = @event.ToState, + Message = @event.Message, + Error = @event.Error, + Timestamp = @event.Timestamp + }; + _dbContext.JobHistoryEntries.Add(historyEntry); + + await _dbContext.SaveChangesAsync(); + + _logger.LogDebug("Recorded job state change: {JobId} {FromState} -> {ToState}", + @event.JobId, @event.FromState, @event.ToState); + } +} diff --git a/FictionArchive.Service.ReportingService/Services/ReportingServiceDbContext.cs b/FictionArchive.Service.ReportingService/Services/ReportingServiceDbContext.cs new file mode 100644 index 0000000..f112bff --- /dev/null +++ b/FictionArchive.Service.ReportingService/Services/ReportingServiceDbContext.cs @@ -0,0 +1,36 @@ +using FictionArchive.Service.ReportingService.Models.Database; +using FictionArchive.Service.Shared.Services.Database; +using Microsoft.EntityFrameworkCore; + +namespace FictionArchive.Service.ReportingService.Services; + +public class ReportingServiceDbContext : FictionArchiveDbContext +{ + public ReportingServiceDbContext(DbContextOptions options, ILogger logger) + : base(options, logger) { } + + public DbSet Jobs => Set(); + public DbSet JobHistoryEntries => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.JobType); + entity.HasIndex(e => e.Status); + entity.HasIndex(e => e.CreatedTime); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasOne(e => e.Job) + .WithMany(j => j.History) + .HasForeignKey(e => e.JobId) + .OnDelete(DeleteBehavior.Cascade); + }); + } +} diff --git a/FictionArchive.Service.ReportingService/appsettings.Development.json b/FictionArchive.Service.ReportingService/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/FictionArchive.Service.ReportingService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/FictionArchive.Service.ReportingService/appsettings.json b/FictionArchive.Service.ReportingService/appsettings.json new file mode 100644 index 0000000..6407457 --- /dev/null +++ b/FictionArchive.Service.ReportingService/appsettings.json @@ -0,0 +1,28 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Database=FictionArchive_ReportingService;Username=postgres;password=postgres" + }, + "RabbitMQ": { + "Host": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" + }, + "OIDC": { + "Authority": "https://auth.orfl.xyz/application/o/fiction-archive/", + "ClientId": "ldi5IpEidq2WW0Ka1lehVskb2SOBjnYRaZCpEyBh", + "Audience": "ldi5IpEidq2WW0Ka1lehVskb2SOBjnYRaZCpEyBh", + "ValidIssuer": "https://auth.orfl.xyz/application/o/fiction-archive/", + "ValidateIssuer": true, + "ValidateAudience": true, + "ValidateLifetime": true, + "ValidateIssuerSigningKey": true + }, + "AllowedHosts": "*" +} diff --git a/FictionArchive.Service.ReportingService/subgraph-config.json b/FictionArchive.Service.ReportingService/subgraph-config.json new file mode 100644 index 0000000..fe97d82 --- /dev/null +++ b/FictionArchive.Service.ReportingService/subgraph-config.json @@ -0,0 +1,6 @@ +{ + "subgraph": "Reporting", + "http": { + "baseAddress": "http://localhost:5180/graphql" + } +} diff --git a/FictionArchive.Service.SchedulerService/FictionArchive.Service.SchedulerService.csproj b/FictionArchive.Service.SchedulerService/FictionArchive.Service.SchedulerService.csproj index 055a4f1..5b32586 100644 --- a/FictionArchive.Service.SchedulerService/FictionArchive.Service.SchedulerService.csproj +++ b/FictionArchive.Service.SchedulerService/FictionArchive.Service.SchedulerService.csproj @@ -19,6 +19,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/FictionArchive.Service.SchedulerService/Models/JobTemplates/EventJobTemplate.cs b/FictionArchive.Service.SchedulerService/Models/JobTemplates/EventJobTemplate.cs index fa4e8fd..a3fdd97 100644 --- a/FictionArchive.Service.SchedulerService/Models/JobTemplates/EventJobTemplate.cs +++ b/FictionArchive.Service.SchedulerService/Models/JobTemplates/EventJobTemplate.cs @@ -1,4 +1,4 @@ -using FictionArchive.Service.Shared.Services.EventBus; +using MassTransit; using Newtonsoft.Json; using Quartz; @@ -6,30 +6,70 @@ namespace FictionArchive.Service.SchedulerService.Models.JobTemplates; public class EventJobTemplate : IJob { - private readonly IEventBus _eventBus; + private readonly IPublishEndpoint _publishEndpoint; + private readonly ISendEndpointProvider _sendEndpointProvider; private readonly ILogger _logger; - public const string EventTypeParameter = "RoutingKey"; + public const string EventTypeParameter = "MessageType"; public const string EventDataParameter = "MessageData"; - - public EventJobTemplate(IEventBus eventBus, ILogger logger) + public const string IsCommandParameter = "IsCommand"; + public const string DestinationQueueParameter = "DestinationQueue"; + + public EventJobTemplate( + IPublishEndpoint publishEndpoint, + ISendEndpointProvider sendEndpointProvider, + ILogger logger) { - _eventBus = eventBus; + _publishEndpoint = publishEndpoint; + _sendEndpointProvider = sendEndpointProvider; _logger = logger; } - + public async Task Execute(IJobExecutionContext context) { try { - var eventData = context.MergedJobDataMap.GetString(EventDataParameter); - var eventType = context.MergedJobDataMap.GetString(EventTypeParameter); - var eventObject = JsonConvert.DeserializeObject(eventData); - await _eventBus.Publish(eventObject, eventType); + var messageData = context.MergedJobDataMap.GetString(EventDataParameter); + var messageTypeName = context.MergedJobDataMap.GetString(EventTypeParameter); + var isCommand = context.MergedJobDataMap.GetBoolean(IsCommandParameter); + + var messageType = Type.GetType(messageTypeName!); + if (messageType == null) + { + _logger.LogError("Could not resolve message type: {MessageType}", messageTypeName); + return; + } + + var message = JsonConvert.DeserializeObject(messageData!, messageType); + if (message == null) + { + _logger.LogError("Could not deserialize message data for type: {MessageType}", messageTypeName); + return; + } + + if (isCommand) + { + var destinationQueue = context.MergedJobDataMap.GetString(DestinationQueueParameter); + if (string.IsNullOrEmpty(destinationQueue)) + { + _logger.LogError("Destination queue not specified for command message"); + return; + } + + var endpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri($"queue:{destinationQueue}")); + await endpoint.Send(message, messageType); + _logger.LogInformation("Sent command {MessageType} to queue {Queue}", messageTypeName, destinationQueue); + } + else + { + await _publishEndpoint.Publish(message, messageType); + _logger.LogInformation("Published event {MessageType}", messageTypeName); + } } catch (Exception ex) { _logger.LogError(ex, "An error occurred while running an event job."); + throw; // Re-throw to let Quartz handle retries } } -} \ No newline at end of file +} diff --git a/FictionArchive.Service.SchedulerService/Program.cs b/FictionArchive.Service.SchedulerService/Program.cs index b1a9c92..6d78676 100644 --- a/FictionArchive.Service.SchedulerService/Program.cs +++ b/FictionArchive.Service.SchedulerService/Program.cs @@ -2,7 +2,7 @@ using FictionArchive.Service.SchedulerService.GraphQL; using FictionArchive.Service.SchedulerService.Services; using FictionArchive.Service.Shared; using FictionArchive.Service.Shared.Extensions; -using FictionArchive.Service.Shared.Services.EventBus.Implementations; +using FictionArchive.Service.Shared.MassTransit; using Quartz; using Quartz.Impl.AdoJobStore; @@ -34,14 +34,11 @@ public class Program #endregion - #region Event Bus + #region MassTransit if (!isSchemaExport) { - builder.Services.AddRabbitMQ(opt => - { - builder.Configuration.GetSection("RabbitMQ").Bind(opt); - }); + builder.Services.AddFictionArchiveMassTransit(builder.Configuration); } #endregion diff --git a/FictionArchive.Service.SchedulerService/Services/JobManagerService.cs b/FictionArchive.Service.SchedulerService/Services/JobManagerService.cs index 1e37f75..fa9404b 100644 --- a/FictionArchive.Service.SchedulerService/Services/JobManagerService.cs +++ b/FictionArchive.Service.SchedulerService/Services/JobManagerService.cs @@ -1,7 +1,6 @@ using System.Data; using FictionArchive.Service.SchedulerService.Models; using FictionArchive.Service.SchedulerService.Models.JobTemplates; -using FictionArchive.Service.Shared.Services.EventBus; using Quartz; using Quartz.Impl.Matchers; diff --git a/FictionArchive.Service.SchedulerService/appsettings.json b/FictionArchive.Service.SchedulerService/appsettings.json index 4b087a3..48ba95d 100644 --- a/FictionArchive.Service.SchedulerService/appsettings.json +++ b/FictionArchive.Service.SchedulerService/appsettings.json @@ -6,8 +6,10 @@ } }, "RabbitMQ": { - "ConnectionString": "amqp://localhost", - "ClientIdentifier": "SchedulerService" + "Host": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" }, "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=FictionArchive_SchedulerService;Username=postgres;password=postgres" diff --git a/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj b/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj index 323b166..ccfdaa1 100644 --- a/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj +++ b/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj @@ -31,7 +31,9 @@ - + + + diff --git a/FictionArchive.Service.Shared/MassTransit/Configuration/MassTransitOptions.cs b/FictionArchive.Service.Shared/MassTransit/Configuration/MassTransitOptions.cs new file mode 100644 index 0000000..15b89c9 --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Configuration/MassTransitOptions.cs @@ -0,0 +1,9 @@ +namespace FictionArchive.Service.Shared.MassTransit.Configuration; + +public class MassTransitOptions +{ + public string Host { get; set; } = "localhost"; + public string VirtualHost { get; set; } = "/"; + public string Username { get; set; } = "guest"; + public string Password { get; set; } = "guest"; +} diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/ImportNovelCommand.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/ImportNovelCommand.cs new file mode 100644 index 0000000..3384ed8 --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/ImportNovelCommand.cs @@ -0,0 +1,6 @@ +namespace FictionArchive.Service.Shared.MassTransit.Contracts.Commands; + +public record ImportNovelCommand : ICommand +{ + public required string NovelUrl { get; init; } +} diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/PullChapterContentCommand.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/PullChapterContentCommand.cs new file mode 100644 index 0000000..a45985c --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/PullChapterContentCommand.cs @@ -0,0 +1,8 @@ +namespace FictionArchive.Service.Shared.MassTransit.Contracts.Commands; + +public record PullChapterContentCommand : ICommand +{ + public required uint NovelId { get; init; } + public required uint VolumeId { get; init; } + public required uint ChapterOrder { get; init; } +} diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/TranslateTextCommand.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/TranslateTextCommand.cs new file mode 100644 index 0000000..72d79df --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/TranslateTextCommand.cs @@ -0,0 +1,12 @@ +using FictionArchive.Common.Enums; + +namespace FictionArchive.Service.Shared.MassTransit.Contracts.Commands; + +public record TranslateTextCommand : ICommand +{ + public Guid TranslationRequestId { get; init; } + public Language From { get; init; } + public Language To { get; init; } + public required string Body { get; init; } + public required string TranslationEngineKey { get; init; } +} diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/UploadFileCommand.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/UploadFileCommand.cs new file mode 100644 index 0000000..1f8c9f9 --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/Commands/UploadFileCommand.cs @@ -0,0 +1,8 @@ +namespace FictionArchive.Service.Shared.MassTransit.Contracts.Commands; + +public record UploadFileCommand : ICommand +{ + public Guid RequestId { get; init; } + public required string FilePath { get; init; } + public required byte[] FileData { get; init; } +} diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/Events/AuthUserAddedEvent.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/AuthUserAddedEvent.cs new file mode 100644 index 0000000..e708b0a --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/AuthUserAddedEvent.cs @@ -0,0 +1,9 @@ +namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events; + +public record AuthUserAddedEvent : IEvent +{ + public required string OAuthProviderId { get; init; } + public required string InviterOAuthProviderId { get; init; } + public required string EventUserEmail { get; init; } + public required string EventUserUsername { get; init; } +} diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/ChapterCreatedEvent.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/ChapterCreatedEvent.cs similarity index 63% rename from FictionArchive.Service.NovelService/Models/IntegrationEvents/ChapterCreatedEvent.cs rename to FictionArchive.Service.Shared/MassTransit/Contracts/Events/ChapterCreatedEvent.cs index 3608c2f..739787b 100644 --- a/FictionArchive.Service.NovelService/Models/IntegrationEvents/ChapterCreatedEvent.cs +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/ChapterCreatedEvent.cs @@ -1,8 +1,6 @@ -using FictionArchive.Service.Shared.Services.EventBus; +namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events; -namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; - -public class ChapterCreatedEvent : IIntegrationEvent +public record ChapterCreatedEvent : IEvent { public required uint ChapterId { get; init; } public required uint NovelId { get; init; } diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/Events/FileUploadCompletedEvent.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/FileUploadCompletedEvent.cs new file mode 100644 index 0000000..74fd555 --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/FileUploadCompletedEvent.cs @@ -0,0 +1,11 @@ +using FictionArchive.Common.Enums; + +namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events; + +public record FileUploadCompletedEvent : IEvent +{ + public Guid RequestId { get; init; } + public RequestStatus Status { get; init; } + public string? FileAccessUrl { get; init; } + public string? ErrorMessage { get; init; } +} diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/NovelCreatedEvent.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/NovelCreatedEvent.cs similarity index 62% rename from FictionArchive.Service.NovelService/Models/IntegrationEvents/NovelCreatedEvent.cs rename to FictionArchive.Service.Shared/MassTransit/Contracts/Events/NovelCreatedEvent.cs index 50ede95..e66ccff 100644 --- a/FictionArchive.Service.NovelService/Models/IntegrationEvents/NovelCreatedEvent.cs +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/NovelCreatedEvent.cs @@ -1,9 +1,8 @@ using FictionArchive.Common.Enums; -using FictionArchive.Service.Shared.Services.EventBus; -namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; +namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events; -public class NovelCreatedEvent : IIntegrationEvent +public record NovelCreatedEvent : IEvent { public required uint NovelId { get; init; } public required string Title { get; init; } diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/Events/TranslationCompletedEvent.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/TranslationCompletedEvent.cs new file mode 100644 index 0000000..5d66168 --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/TranslationCompletedEvent.cs @@ -0,0 +1,7 @@ +namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events; + +public record TranslationCompletedEvent : IEvent +{ + public Guid TranslationRequestId { get; init; } + public required string TranslatedText { get; init; } +} diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/Events/UserInvitedEvent.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/UserInvitedEvent.cs new file mode 100644 index 0000000..59e6ccf --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/Events/UserInvitedEvent.cs @@ -0,0 +1,13 @@ +namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events; + +public record UserInvitedEvent : IEvent +{ + public Guid InvitedUserId { get; init; } + public required string InvitedUsername { get; init; } + public required string InvitedEmail { get; init; } + public required string InvitedOAuthProviderId { get; init; } + + public Guid InviterId { get; init; } + public required string InviterUsername { get; init; } + public required string InviterOAuthProviderId { get; init; } +} diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/ICommand.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/ICommand.cs new file mode 100644 index 0000000..21b9c9a --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/ICommand.cs @@ -0,0 +1,6 @@ +namespace FictionArchive.Service.Shared.MassTransit.Contracts; + +/// +/// Marker interface for commands (do something, single consumer) +/// +public interface ICommand { } diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/IEvent.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/IEvent.cs new file mode 100644 index 0000000..cd3a21d --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/IEvent.cs @@ -0,0 +1,6 @@ +namespace FictionArchive.Service.Shared.MassTransit.Contracts; + +/// +/// Marker interface for events (something happened, multiple subscribers) +/// +public interface IEvent { } diff --git a/FictionArchive.Service.Shared/MassTransit/Contracts/JobStateChangedEvent.cs b/FictionArchive.Service.Shared/MassTransit/Contracts/JobStateChangedEvent.cs new file mode 100644 index 0000000..879373b --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/Contracts/JobStateChangedEvent.cs @@ -0,0 +1,18 @@ +using NodaTime; + +namespace FictionArchive.Service.Shared.MassTransit.Contracts; + +/// +/// Published by sagas on state transitions for centralized job tracking +/// +public record JobStateChangedEvent : IEvent +{ + public Guid JobId { get; init; } + public required string JobType { get; init; } + public required string FromState { get; init; } + public required string ToState { get; init; } + public string? Message { get; init; } + public string? Error { get; init; } + public Instant Timestamp { get; init; } + public Dictionary? Metadata { get; init; } +} diff --git a/FictionArchive.Service.Shared/MassTransit/MassTransitExtensions.cs b/FictionArchive.Service.Shared/MassTransit/MassTransitExtensions.cs new file mode 100644 index 0000000..b3d54b5 --- /dev/null +++ b/FictionArchive.Service.Shared/MassTransit/MassTransitExtensions.cs @@ -0,0 +1,103 @@ +using FictionArchive.Service.Shared.MassTransit.Configuration; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace FictionArchive.Service.Shared.MassTransit; + +public static class MassTransitExtensions +{ + /// + /// Adds MassTransit with RabbitMQ and Entity Framework outbox + /// + public static IServiceCollection AddFictionArchiveMassTransit( + this IServiceCollection services, + IConfiguration configuration, + Action? configureConsumers = null) + where TDbContext : DbContext + { + services.AddMassTransit(x => + { + configureConsumers?.Invoke(x); + + x.AddEntityFrameworkOutbox(o => + { + o.UsePostgres(); + o.UseBusOutbox(); + }); + + x.UsingRabbitMq((context, cfg) => + { + var options = configuration.GetSection("RabbitMQ").Get() + ?? new MassTransitOptions(); + + cfg.Host(options.Host, options.VirtualHost, h => + { + h.Username(options.Username); + h.Password(options.Password); + }); + + // Immediate retries for transient failures + cfg.UseMessageRetry(r => r.Intervals( + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1))); + + // Delayed redelivery for longer outages + cfg.UseDelayedRedelivery(r => r.Intervals( + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(30), + TimeSpan.FromMinutes(2), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(30))); + + cfg.ConfigureEndpoints(context); + }); + }); + + return services; + } + + /// + /// Adds MassTransit with RabbitMQ without outbox (for services without EF) + /// + public static IServiceCollection AddFictionArchiveMassTransit( + this IServiceCollection services, + IConfiguration configuration, + Action? configureConsumers = null) + { + services.AddMassTransit(x => + { + configureConsumers?.Invoke(x); + + x.UsingRabbitMq((context, cfg) => + { + var options = configuration.GetSection("RabbitMQ").Get() + ?? new MassTransitOptions(); + + cfg.Host(options.Host, options.VirtualHost, h => + { + h.Username(options.Username); + h.Password(options.Password); + }); + + cfg.UseMessageRetry(r => r.Intervals( + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1))); + + cfg.UseDelayedRedelivery(r => r.Intervals( + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(30), + TimeSpan.FromMinutes(2), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(30))); + + cfg.ConfigureEndpoints(context); + }); + }); + + return services; + } +} diff --git a/FictionArchive.Service.Shared/Services/EventBus/EventBusBuilder.cs b/FictionArchive.Service.Shared/Services/EventBus/EventBusBuilder.cs deleted file mode 100644 index 1331fba..0000000 --- a/FictionArchive.Service.Shared/Services/EventBus/EventBusBuilder.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace FictionArchive.Service.Shared.Services.EventBus; - -public class EventBusBuilder where TEventBus : class, IEventBus -{ - private readonly IServiceCollection _services; - private readonly SubscriptionManager _subscriptionManager; - - public EventBusBuilder(IServiceCollection services) - { - _services = services; - _services.AddSingleton(); - - _subscriptionManager = new SubscriptionManager(); - _services.AddSingleton(_subscriptionManager); - } - - public EventBusBuilder Subscribe() where TEvent : IIntegrationEvent where TEventHandler : class, IIntegrationEventHandler - { - _services.AddKeyedTransient(typeof(TEvent).Name); - _subscriptionManager.RegisterSubscription(); - return this; - } -} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/EventBusExtensions.cs b/FictionArchive.Service.Shared/Services/EventBus/EventBusExtensions.cs deleted file mode 100644 index 417bfa8..0000000 --- a/FictionArchive.Service.Shared/Services/EventBus/EventBusExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace FictionArchive.Service.Shared.Services.EventBus; - -public static class EventBusExtensions -{ - public static EventBusBuilder AddEventBus(this IServiceCollection services) - where TEventBus : class, IEventBus - { - return new EventBusBuilder(services); - } -} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/IEventBus.cs b/FictionArchive.Service.Shared/Services/EventBus/IEventBus.cs deleted file mode 100644 index 46b28eb..0000000 --- a/FictionArchive.Service.Shared/Services/EventBus/IEventBus.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FictionArchive.Service.Shared.Services.EventBus; - -public interface IEventBus -{ - Task Publish(TEvent integrationEvent) where TEvent : IIntegrationEvent; - Task Publish(object integrationEvent, string eventType); -} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/IIntegrationEvent.cs b/FictionArchive.Service.Shared/Services/EventBus/IIntegrationEvent.cs deleted file mode 100644 index 499f92c..0000000 --- a/FictionArchive.Service.Shared/Services/EventBus/IIntegrationEvent.cs +++ /dev/null @@ -1,7 +0,0 @@ -using NodaTime; - -namespace FictionArchive.Service.Shared.Services.EventBus; - -public interface IIntegrationEvent -{ -} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/IIntegrationEventHandler.cs b/FictionArchive.Service.Shared/Services/EventBus/IIntegrationEventHandler.cs deleted file mode 100644 index 0541637..0000000 --- a/FictionArchive.Service.Shared/Services/EventBus/IIntegrationEventHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FictionArchive.Service.Shared.Services.EventBus; - -public interface IIntegrationEventHandler : IIntegrationEventHandler where TEvent : IIntegrationEvent -{ - Task Handle(TEvent @event); - Task IIntegrationEventHandler.Handle(IIntegrationEvent @event) => Handle((TEvent)@event); -} - -public interface IIntegrationEventHandler -{ - Task Handle(IIntegrationEvent @event); -} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQConnectionProvider.cs b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQConnectionProvider.cs deleted file mode 100644 index ad93539..0000000 --- a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQConnectionProvider.cs +++ /dev/null @@ -1,35 +0,0 @@ -using RabbitMQ.Client; - -namespace FictionArchive.Service.Shared.Services.EventBus.Implementations; - -public class RabbitMQConnectionProvider -{ - private readonly IConnectionFactory _connectionFactory; - - private IConnection Connection { get; set; } - private IChannel DefaultChannel { get; set; } - - public RabbitMQConnectionProvider(IConnectionFactory connectionFactory) - { - _connectionFactory = connectionFactory; - } - - public async Task GetConnectionAsync() - { - if (Connection == null) - { - Connection = await _connectionFactory.CreateConnectionAsync(); - } - - return Connection; - } - - public async Task GetDefaultChannelAsync() - { - if (DefaultChannel == null) - { - DefaultChannel = await (await GetConnectionAsync()).CreateChannelAsync(); - } - return DefaultChannel; - } -} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQEventBus.cs b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQEventBus.cs deleted file mode 100644 index 758f2d8..0000000 --- a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQEventBus.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System.Text; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using NodaTime; -using NodaTime.Serialization.JsonNet; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; - -namespace FictionArchive.Service.Shared.Services.EventBus.Implementations; - -public class RabbitMQEventBus : IEventBus, IHostedService -{ - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly RabbitMQConnectionProvider _connectionProvider; - private readonly RabbitMQOptions _options; - private readonly SubscriptionManager _subscriptionManager; - private readonly ILogger _logger; - - private readonly JsonSerializerSettings _jsonSerializerSettings; - - private const string ExchangeName = "fiction-archive-event-bus"; - private const string CreatedAtHeader = "X-Created-At"; - private const string EventIdHeader = "X-Event-Id"; - - public RabbitMQEventBus(IServiceScopeFactory serviceScopeFactory, RabbitMQConnectionProvider connectionProvider, IOptions options, SubscriptionManager subscriptionManager, ILogger logger) - { - _serviceScopeFactory = serviceScopeFactory; - _connectionProvider = connectionProvider; - _subscriptionManager = subscriptionManager; - _logger = logger; - _options = options.Value; - _jsonSerializerSettings = new JsonSerializerSettings().ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - } - - public async Task Publish(TEvent integrationEvent) where TEvent : IIntegrationEvent - { - var routingKey = typeof(TEvent).Name; - - await Publish(integrationEvent, routingKey); - } - - public async Task Publish(object integrationEvent, string eventType) - { - var channel = await _connectionProvider.GetDefaultChannelAsync(); - var body = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(integrationEvent)); - - // headers - var props = new BasicProperties(); - props.Headers = new Dictionary() - { - { CreatedAtHeader, Instant.FromDateTimeUtc(DateTime.UtcNow).ToString() }, - { EventIdHeader, Guid.NewGuid().ToString() } - }; - - await channel.BasicPublishAsync(ExchangeName, eventType, true, props, body); - _logger.LogInformation("Published event {EventName}", eventType); - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - _ = Task.Factory.StartNew(async () => - { - try - { - var channel = await _connectionProvider.GetDefaultChannelAsync(); - await channel.ExchangeDeclareAsync(ExchangeName, ExchangeType.Direct, - cancellationToken: cancellationToken); - - await channel.BasicQosAsync(prefetchSize: 0, prefetchCount: 1, global: false, cancellationToken: cancellationToken); - await channel.QueueDeclareAsync(_options.ClientIdentifier, true, false, false, - cancellationToken: cancellationToken); - var consumer = new AsyncEventingBasicConsumer(channel); - consumer.ReceivedAsync += (sender, @event) => - { - return OnReceivedEvent(sender, @event, channel); - }; - - await channel.BasicConsumeAsync(_options.ClientIdentifier, false, consumer, cancellationToken: cancellationToken); - - foreach (var subscription in _subscriptionManager.Subscriptions) - { - await channel.QueueBindAsync(_options.ClientIdentifier, ExchangeName, subscription.Key, - cancellationToken: cancellationToken); - _logger.LogInformation("Subscribed to {SubscriptionKey}", subscription.Key); - } - - _logger.LogInformation("RabbitMQ EventBus started."); - } - catch (Exception e) - { - _logger.LogError(e, "An error occurred while starting the RabbitMQ EventBus"); - } - }, cancellationToken); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - private async Task OnReceivedEvent(object sender, BasicDeliverEventArgs @event, IChannel channel) - { - var eventName = @event.RoutingKey; - _logger.LogInformation("Received event {EventName}", eventName); - try - { - if (!_subscriptionManager.Subscriptions.ContainsKey(eventName)) - { - _logger.LogWarning("Received event without subscription entry."); - return; - } - - var eventBody = Encoding.UTF8.GetString(@event.Body.Span); - var eventObject = JsonConvert.DeserializeObject(eventBody, _subscriptionManager.Subscriptions[eventName], _jsonSerializerSettings) as IIntegrationEvent; - - using var scope = _serviceScopeFactory.CreateScope(); - - foreach (var service in scope.ServiceProvider.GetKeyedServices(eventName)) - { - await service.Handle(eventObject); - } - _logger.LogInformation("Finished handling event with name {EventName}", eventName); - } - catch (Exception e) - { - _logger.LogError(e, "An error occurred while handling an event."); - } - finally - { - await channel.BasicAckAsync(@event.DeliveryTag, false); - } - - } -} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQExtensions.cs b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQExtensions.cs deleted file mode 100644 index 14546d4..0000000 --- a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using RabbitMQ.Client; - -namespace FictionArchive.Service.Shared.Services.EventBus.Implementations; - -public static class RabbitMQExtensions -{ - public static EventBusBuilder AddRabbitMQ(this IServiceCollection services, Action configure) - { - services.Configure(configure); - services.AddSingleton(provider => - { - var options = provider.GetService>(); - ConnectionFactory factory = new ConnectionFactory(); - factory.Uri = new Uri(options.Value.ConnectionString); - return factory; - }); - services.AddSingleton(); - services.AddHostedService(); - return services.AddEventBus(); - } -} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQOptions.cs b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQOptions.cs deleted file mode 100644 index f385a35..0000000 --- a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FictionArchive.Service.Shared.Services.EventBus.Implementations; - -public class RabbitMQOptions -{ - public string ConnectionString { get; set; } - public string ClientIdentifier { get; set; } -} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/SubscriptionManager.cs b/FictionArchive.Service.Shared/Services/EventBus/SubscriptionManager.cs deleted file mode 100644 index 73b8c37..0000000 --- a/FictionArchive.Service.Shared/Services/EventBus/SubscriptionManager.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace FictionArchive.Service.Shared.Services.EventBus; - -public class SubscriptionManager -{ - public Dictionary Subscriptions { get; } = new Dictionary(); - - public void RegisterSubscription() - { - Subscriptions.Add(typeof(TEvent).Name, typeof(TEvent)); - } -} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/FictionArchive.Service.TranslationService.csproj b/FictionArchive.Service.TranslationService/FictionArchive.Service.TranslationService.csproj index cd4d3c3..88111c3 100644 --- a/FictionArchive.Service.TranslationService/FictionArchive.Service.TranslationService.csproj +++ b/FictionArchive.Service.TranslationService/FictionArchive.Service.TranslationService.csproj @@ -23,6 +23,7 @@ + diff --git a/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs b/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs deleted file mode 100644 index e2ead5f..0000000 --- a/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FictionArchive.Common.Enums; -using FictionArchive.Service.Shared.Services.EventBus; -using FictionArchive.Service.TranslationService.Models.Enums; - -namespace FictionArchive.Service.TranslationService.Models.IntegrationEvents; - -public class TranslationRequestCompletedEvent : IIntegrationEvent -{ - /// - /// Maps this event back to a triggering request. - /// - public Guid? TranslationRequestId { get; set; } - - /// - /// The resulting text. - /// - public string? TranslatedText { get; set; } -} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs b/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs deleted file mode 100644 index 4d627cb..0000000 --- a/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FictionArchive.Common.Enums; -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.TranslationService.Models.IntegrationEvents; - -public class TranslationRequestCreatedEvent : IIntegrationEvent -{ - public Guid TranslationRequestId { get; set; } - public Language From { get; set; } - public Language To { get; set; } - public string Body { get; set; } - public string TranslationEngineKey { get; set; } -} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Program.cs b/FictionArchive.Service.TranslationService/Program.cs index 9f4972c..83a6e20 100644 --- a/FictionArchive.Service.TranslationService/Program.cs +++ b/FictionArchive.Service.TranslationService/Program.cs @@ -2,16 +2,14 @@ using DeepL; using FictionArchive.Common.Extensions; using FictionArchive.Service.Shared; using FictionArchive.Service.Shared.Extensions; -using FictionArchive.Service.Shared.Services.EventBus.Implementations; +using FictionArchive.Service.Shared.MassTransit; using FictionArchive.Service.Shared.Services.GraphQL; using FictionArchive.Service.TranslationService.GraphQL; -using FictionArchive.Service.TranslationService.Models.IntegrationEvents; using FictionArchive.Service.TranslationService.Services; using FictionArchive.Service.TranslationService.Services.Database; using FictionArchive.Service.TranslationService.Services.EventHandlers; using FictionArchive.Service.TranslationService.Services.TranslationEngines; using FictionArchive.Service.TranslationService.Services.TranslationEngines.DeepLTranslate; -using RabbitMQ.Client; namespace FictionArchive.Service.TranslationService; @@ -26,15 +24,16 @@ public class Program builder.Services.AddHealthChecks(); - #region Event Bus + #region MassTransit if (!isSchemaExport) { - builder.Services.AddRabbitMQ(opt => - { - builder.Configuration.GetSection("RabbitMQ").Bind(opt); - }) - .Subscribe(); + builder.Services.AddFictionArchiveMassTransit( + builder.Configuration, + x => + { + x.AddConsumer(); + }); } #endregion diff --git a/FictionArchive.Service.TranslationService/Services/EventHandlers/TranslateTextCommandConsumer.cs b/FictionArchive.Service.TranslationService/Services/EventHandlers/TranslateTextCommandConsumer.cs new file mode 100644 index 0000000..5f82fba --- /dev/null +++ b/FictionArchive.Service.TranslationService/Services/EventHandlers/TranslateTextCommandConsumer.cs @@ -0,0 +1,40 @@ +using FictionArchive.Service.Shared.MassTransit.Contracts.Commands; +using FictionArchive.Service.Shared.MassTransit.Contracts.Events; +using FictionArchive.Service.TranslationService.Models.Enums; +using MassTransit; + +namespace FictionArchive.Service.TranslationService.Services.EventHandlers; + +public class TranslateTextCommandConsumer : IConsumer +{ + private readonly ILogger _logger; + private readonly TranslationEngineService _translationEngineService; + + public TranslateTextCommandConsumer( + ILogger logger, + TranslationEngineService translationEngineService) + { + _logger = logger; + _translationEngineService = translationEngineService; + } + + public async Task Consume(ConsumeContext context) + { + var command = context.Message; + + var result = await _translationEngineService.Translate( + command.From, + command.To, + command.Body, + command.TranslationEngineKey); + + if (result.Status == TranslationRequestStatus.Success) + { + await context.Publish(new TranslationCompletedEvent + { + TranslatedText = result.TranslatedText!, + TranslationRequestId = command.TranslationRequestId, + }); + } + } +} diff --git a/FictionArchive.Service.TranslationService/Services/EventHandlers/TranslationRequestCreatedEventHandler.cs b/FictionArchive.Service.TranslationService/Services/EventHandlers/TranslationRequestCreatedEventHandler.cs deleted file mode 100644 index a824d34..0000000 --- a/FictionArchive.Service.TranslationService/Services/EventHandlers/TranslationRequestCreatedEventHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using FictionArchive.Service.Shared.Services.EventBus; -using FictionArchive.Service.TranslationService.Models.Enums; -using FictionArchive.Service.TranslationService.Models.IntegrationEvents; - -namespace FictionArchive.Service.TranslationService.Services.EventHandlers; - -public class TranslationRequestCreatedEventHandler : IIntegrationEventHandler -{ - private readonly ILogger _logger; - private readonly TranslationEngineService _translationEngineService; - private readonly IEventBus _eventBus; - - public TranslationRequestCreatedEventHandler(ILogger logger, TranslationEngineService translationEngineService) - { - _logger = logger; - _translationEngineService = translationEngineService; - } - - public async Task Handle(TranslationRequestCreatedEvent @event) - { - var result = await _translationEngineService.Translate(@event.From, @event.To, @event.Body, @event.TranslationEngineKey); - if (result.Status == TranslationRequestStatus.Success) - { - await _eventBus.Publish(new TranslationRequestCompletedEvent() - { - TranslatedText = result.TranslatedText, - TranslationRequestId = @event.TranslationRequestId, - }); - } - } -} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Services/TranslationEngineService.cs b/FictionArchive.Service.TranslationService/Services/TranslationEngineService.cs index 3768704..7db19a3 100644 --- a/FictionArchive.Service.TranslationService/Services/TranslationEngineService.cs +++ b/FictionArchive.Service.TranslationService/Services/TranslationEngineService.cs @@ -1,28 +1,21 @@ -using System.Text; using FictionArchive.Common.Enums; -using FictionArchive.Service.Shared.Services.EventBus; -using FictionArchive.Service.Shared.Services.EventBus.Implementations; using FictionArchive.Service.TranslationService.Models; using FictionArchive.Service.TranslationService.Models.Database; using FictionArchive.Service.TranslationService.Models.Enums; -using FictionArchive.Service.TranslationService.Models.IntegrationEvents; using FictionArchive.Service.TranslationService.Services.Database; using FictionArchive.Service.TranslationService.Services.TranslationEngines; -using RabbitMQ.Client; namespace FictionArchive.Service.TranslationService.Services; public class TranslationEngineService { private readonly IEnumerable _translationEngines; - private readonly IEventBus _eventBus; private readonly TranslationServiceDbContext _dbContext; - public TranslationEngineService(IEnumerable translationEngines, TranslationServiceDbContext dbContext, IEventBus eventBus) + public TranslationEngineService(IEnumerable translationEngines, TranslationServiceDbContext dbContext) { _translationEngines = translationEngines; _dbContext = dbContext; - _eventBus = eventBus; } public async Task Translate(Language from, Language to, string text, string translationEngineKey) diff --git a/FictionArchive.Service.TranslationService/appsettings.json b/FictionArchive.Service.TranslationService/appsettings.json index 7f2534b..9649a0c 100644 --- a/FictionArchive.Service.TranslationService/appsettings.json +++ b/FictionArchive.Service.TranslationService/appsettings.json @@ -12,8 +12,10 @@ "DefaultConnection": "Host=localhost;Database=FictionArchive_NovelService;Username=postgres;password=postgres" }, "RabbitMQ": { - "ConnectionString": "amqp://localhost", - "ClientIdentifier": "TranslationService" + "Host": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" }, "AllowedHosts": "*", "OIDC": { diff --git a/FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/ChapterCreatedEvent.cs b/FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/ChapterCreatedEvent.cs deleted file mode 100644 index 2591f68..0000000 --- a/FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/ChapterCreatedEvent.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.UserNovelDataService.Models.IntegrationEvents; - -public class ChapterCreatedEvent : IIntegrationEvent -{ - public required uint ChapterId { get; init; } - public required uint NovelId { get; init; } - public required uint VolumeId { get; init; } - public required int VolumeOrder { get; init; } - public required uint ChapterOrder { get; init; } - public required string ChapterTitle { get; init; } -} diff --git a/FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/NovelCreatedEvent.cs b/FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/NovelCreatedEvent.cs deleted file mode 100644 index f55c349..0000000 --- a/FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/NovelCreatedEvent.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FictionArchive.Common.Enums; -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.UserNovelDataService.Models.IntegrationEvents; - -public class NovelCreatedEvent : IIntegrationEvent -{ - public required uint NovelId { get; init; } - public required string Title { get; init; } - public required Language OriginalLanguage { get; init; } - public required string Source { get; init; } - public required string AuthorName { get; init; } -} diff --git a/FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/UserInvitedEvent.cs b/FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/UserInvitedEvent.cs deleted file mode 100644 index 609f029..0000000 --- a/FictionArchive.Service.UserNovelDataService/Models/IntegrationEvents/UserInvitedEvent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.UserNovelDataService.Models.IntegrationEvents; - -public class UserInvitedEvent : IIntegrationEvent -{ - public Guid InvitedUserId { get; set; } - public required string InvitedUsername { get; set; } - public required string InvitedEmail { get; set; } - public required string InvitedOAuthProviderId { get; set; } - - public Guid InviterId { get; set; } - public required string InviterUsername { get; set; } - public required string InviterOAuthProviderId { get; set; } -} diff --git a/FictionArchive.Service.UserNovelDataService/Program.cs b/FictionArchive.Service.UserNovelDataService/Program.cs index d14cc0e..6e65c1f 100644 --- a/FictionArchive.Service.UserNovelDataService/Program.cs +++ b/FictionArchive.Service.UserNovelDataService/Program.cs @@ -1,9 +1,8 @@ using FictionArchive.Common.Extensions; using FictionArchive.Service.Shared; using FictionArchive.Service.Shared.Extensions; -using FictionArchive.Service.Shared.Services.EventBus.Implementations; +using FictionArchive.Service.Shared.MassTransit; using FictionArchive.Service.UserNovelDataService.GraphQL; -using FictionArchive.Service.UserNovelDataService.Models.IntegrationEvents; using FictionArchive.Service.UserNovelDataService.Services; using FictionArchive.Service.UserNovelDataService.Services.EventHandlers; @@ -22,17 +21,18 @@ public class Program builder.Services.AddMemoryCache(); builder.Services.AddHealthChecks(); - #region Event Bus + #region MassTransit if (!isSchemaExport) { - builder.Services.AddRabbitMQ(opt => - { - builder.Configuration.GetSection("RabbitMQ").Bind(opt); - }) - .Subscribe() - .Subscribe() - .Subscribe(); + builder.Services.AddFictionArchiveMassTransit( + builder.Configuration, + x => + { + x.AddConsumer(); + x.AddConsumer(); + x.AddConsumer(); + }); } #endregion diff --git a/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/ChapterCreatedEventHandler.cs b/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/ChapterCreatedEventConsumer.cs similarity index 77% rename from FictionArchive.Service.UserNovelDataService/Services/EventHandlers/ChapterCreatedEventHandler.cs rename to FictionArchive.Service.UserNovelDataService/Services/EventHandlers/ChapterCreatedEventConsumer.cs index 74f46f7..470c93b 100644 --- a/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/ChapterCreatedEventHandler.cs +++ b/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/ChapterCreatedEventConsumer.cs @@ -1,25 +1,27 @@ -using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.Shared.MassTransit.Contracts.Events; using FictionArchive.Service.UserNovelDataService.Models.Database; -using FictionArchive.Service.UserNovelDataService.Models.IntegrationEvents; +using MassTransit; using Microsoft.EntityFrameworkCore; namespace FictionArchive.Service.UserNovelDataService.Services.EventHandlers; -public class ChapterCreatedEventHandler : IIntegrationEventHandler +public class ChapterCreatedEventConsumer : IConsumer { private readonly UserNovelDataServiceDbContext _dbContext; - private readonly ILogger _logger; + private readonly ILogger _logger; - public ChapterCreatedEventHandler( + public ChapterCreatedEventConsumer( UserNovelDataServiceDbContext dbContext, - ILogger logger) + ILogger logger) { _dbContext = dbContext; _logger = logger; } - public async Task Handle(ChapterCreatedEvent @event) + public async Task Consume(ConsumeContext context) { + var @event = context.Message; + // Ensure novel exists var novelExists = await _dbContext.Novels.AnyAsync(n => n.Id == @event.NovelId); if (!novelExists) diff --git a/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/NovelCreatedEventHandler.cs b/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/NovelCreatedEventConsumer.cs similarity index 66% rename from FictionArchive.Service.UserNovelDataService/Services/EventHandlers/NovelCreatedEventHandler.cs rename to FictionArchive.Service.UserNovelDataService/Services/EventHandlers/NovelCreatedEventConsumer.cs index 1e47531..ba75cbf 100644 --- a/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/NovelCreatedEventHandler.cs +++ b/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/NovelCreatedEventConsumer.cs @@ -1,25 +1,27 @@ -using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.Shared.MassTransit.Contracts.Events; using FictionArchive.Service.UserNovelDataService.Models.Database; -using FictionArchive.Service.UserNovelDataService.Models.IntegrationEvents; +using MassTransit; using Microsoft.EntityFrameworkCore; namespace FictionArchive.Service.UserNovelDataService.Services.EventHandlers; -public class NovelCreatedEventHandler : IIntegrationEventHandler +public class NovelCreatedEventConsumer : IConsumer { private readonly UserNovelDataServiceDbContext _dbContext; - private readonly ILogger _logger; + private readonly ILogger _logger; - public NovelCreatedEventHandler( + public NovelCreatedEventConsumer( UserNovelDataServiceDbContext dbContext, - ILogger logger) + ILogger logger) { _dbContext = dbContext; _logger = logger; } - public async Task Handle(NovelCreatedEvent @event) + public async Task Consume(ConsumeContext context) { + var @event = context.Message; + var exists = await _dbContext.Novels.AnyAsync(n => n.Id == @event.NovelId); if (exists) { diff --git a/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/UserInvitedEventHandler.cs b/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/UserInvitedEventConsumer.cs similarity index 69% rename from FictionArchive.Service.UserNovelDataService/Services/EventHandlers/UserInvitedEventHandler.cs rename to FictionArchive.Service.UserNovelDataService/Services/EventHandlers/UserInvitedEventConsumer.cs index a48a2c8..19fb219 100644 --- a/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/UserInvitedEventHandler.cs +++ b/FictionArchive.Service.UserNovelDataService/Services/EventHandlers/UserInvitedEventConsumer.cs @@ -1,25 +1,27 @@ -using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.Shared.MassTransit.Contracts.Events; using FictionArchive.Service.UserNovelDataService.Models.Database; -using FictionArchive.Service.UserNovelDataService.Models.IntegrationEvents; +using MassTransit; using Microsoft.EntityFrameworkCore; namespace FictionArchive.Service.UserNovelDataService.Services.EventHandlers; -public class UserInvitedEventHandler : IIntegrationEventHandler +public class UserInvitedEventConsumer : IConsumer { private readonly UserNovelDataServiceDbContext _dbContext; - private readonly ILogger _logger; + private readonly ILogger _logger; - public UserInvitedEventHandler( + public UserInvitedEventConsumer( UserNovelDataServiceDbContext dbContext, - ILogger logger) + ILogger logger) { _dbContext = dbContext; _logger = logger; } - public async Task Handle(UserInvitedEvent @event) + public async Task Consume(ConsumeContext context) { + var @event = context.Message; + var exists = await _dbContext.Users.AnyAsync(u => u.Id == @event.InvitedUserId); if (exists) { diff --git a/FictionArchive.Service.UserNovelDataService/appsettings.json b/FictionArchive.Service.UserNovelDataService/appsettings.json index 425535a..5e8ebfc 100644 --- a/FictionArchive.Service.UserNovelDataService/appsettings.json +++ b/FictionArchive.Service.UserNovelDataService/appsettings.json @@ -9,8 +9,10 @@ "DefaultConnection": "Host=localhost;Database=FictionArchive_UserNovelDataService;Username=postgres;password=postgres" }, "RabbitMQ": { - "ConnectionString": "amqp://localhost", - "ClientIdentifier": "UserNovelDataService" + "Host": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" }, "OIDC": { "Authority": "https://auth.orfl.xyz/application/o/fiction-archive/", diff --git a/FictionArchive.Service.UserService.Tests/UserManagementServiceTests.cs b/FictionArchive.Service.UserService.Tests/UserManagementServiceTests.cs index 9050d79..9233141 100644 --- a/FictionArchive.Service.UserService.Tests/UserManagementServiceTests.cs +++ b/FictionArchive.Service.UserService.Tests/UserManagementServiceTests.cs @@ -1,9 +1,9 @@ -using FictionArchive.Service.Shared.Services.EventBus; using FictionArchive.Service.UserService.Models.Database; using FictionArchive.Service.UserService.Services; using FictionArchive.Service.UserService.Services.AuthenticationClient; using FictionArchive.Service.UserService.Services.AuthenticationClient.Authentik; using FluentAssertions; +using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; @@ -27,13 +27,13 @@ public class UserManagementServiceTests private static UserManagementService CreateService( UserServiceDbContext dbContext, IAuthenticationServiceClient authClient, - IEventBus? eventBus = null) + IPublishEndpoint? publishEndpoint = null) { return new UserManagementService( dbContext, NullLogger.Instance, authClient, - eventBus ?? Substitute.For()); + publishEndpoint ?? Substitute.For()); } private static User CreateTestUser(string username, string email, int availableInvites = 5) diff --git a/FictionArchive.Service.UserService/Models/IntegrationEvents/UserInvitedEvent.cs b/FictionArchive.Service.UserService/Models/IntegrationEvents/UserInvitedEvent.cs deleted file mode 100644 index 5039e75..0000000 --- a/FictionArchive.Service.UserService/Models/IntegrationEvents/UserInvitedEvent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using FictionArchive.Service.Shared.Services.EventBus; - -namespace FictionArchive.Service.UserService.Models.IntegrationEvents; - -public class UserInvitedEvent : IIntegrationEvent -{ - // Invited user info - public Guid InvitedUserId { get; set; } - public required string InvitedUsername { get; set; } - public required string InvitedEmail { get; set; } - public required string InvitedOAuthProviderId { get; set; } - - // Inviter info - public Guid InviterId { get; set; } - public required string InviterUsername { get; set; } - public required string InviterOAuthProviderId { get; set; } -} diff --git a/FictionArchive.Service.UserService/Program.cs b/FictionArchive.Service.UserService/Program.cs index 419c199..0e0285c 100644 --- a/FictionArchive.Service.UserService/Program.cs +++ b/FictionArchive.Service.UserService/Program.cs @@ -2,7 +2,7 @@ using System.Net.Http.Headers; using FictionArchive.Common.Extensions; using FictionArchive.Service.Shared; using FictionArchive.Service.Shared.Extensions; -using FictionArchive.Service.Shared.Services.EventBus.Implementations; +using FictionArchive.Service.Shared.MassTransit; using FictionArchive.Service.UserService.GraphQL; using FictionArchive.Service.UserService.Services; using FictionArchive.Service.UserService.Services.AuthenticationClient; @@ -19,14 +19,11 @@ public class Program var builder = WebApplication.CreateBuilder(args); builder.AddLocalAppsettings(); - #region Event Bus + #region MassTransit if (!isSchemaExport) { - builder.Services.AddRabbitMQ(opt => - { - builder.Configuration.GetSection("RabbitMQ").Bind(opt); - }); + builder.Services.AddFictionArchiveMassTransit(builder.Configuration); } #endregion diff --git a/FictionArchive.Service.UserService/Services/UserManagementService.cs b/FictionArchive.Service.UserService/Services/UserManagementService.cs index 6a5a496..e788917 100644 --- a/FictionArchive.Service.UserService/Services/UserManagementService.cs +++ b/FictionArchive.Service.UserService/Services/UserManagementService.cs @@ -1,7 +1,7 @@ -using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.Shared.MassTransit.Contracts.Events; using FictionArchive.Service.UserService.Models.Database; -using FictionArchive.Service.UserService.Models.IntegrationEvents; using FictionArchive.Service.UserService.Services.AuthenticationClient; +using MassTransit; using Microsoft.EntityFrameworkCore; namespace FictionArchive.Service.UserService.Services; @@ -11,18 +11,18 @@ public class UserManagementService private readonly ILogger _logger; private readonly UserServiceDbContext _dbContext; private readonly IAuthenticationServiceClient _authClient; - private readonly IEventBus _eventBus; + private readonly IPublishEndpoint _publishEndpoint; public UserManagementService( UserServiceDbContext dbContext, ILogger logger, IAuthenticationServiceClient authClient, - IEventBus eventBus) + IPublishEndpoint publishEndpoint) { _dbContext = dbContext; _logger = logger; _authClient = authClient; - _eventBus = eventBus; + _publishEndpoint = publishEndpoint; } /// @@ -99,7 +99,7 @@ public class UserManagementService await _dbContext.SaveChangesAsync(); - await _eventBus.Publish(new UserInvitedEvent + await _publishEndpoint.Publish(new UserInvitedEvent { InvitedUserId = newUser.Id, InvitedUsername = newUser.Username, diff --git a/FictionArchive.Service.UserService/appsettings.json b/FictionArchive.Service.UserService/appsettings.json index 2182f2f..6f9383a 100644 --- a/FictionArchive.Service.UserService/appsettings.json +++ b/FictionArchive.Service.UserService/appsettings.json @@ -9,8 +9,10 @@ "DefaultConnection": "Host=localhost;Database=FictionArchive_UserService;Username=postgres;password=postgres" }, "RabbitMQ": { - "ConnectionString": "amqp://localhost", - "ClientIdentifier": "UserService" + "Host": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest" }, "Authentik": { "BaseUrl": "https://auth.orfl.xyz", diff --git a/FictionArchive.sln b/FictionArchive.sln index 8d09302..93bf675 100644 --- a/FictionArchive.sln +++ b/FictionArchive.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# +# Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Common", "FictionArchive.Common\FictionArchive.Common.csproj", "{ABF1BA10-9E76-45BE-9947-E20445A68147}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.API", "FictionArchive.API\FictionArchive.API.csproj", "{420CC1A1-9DBC-40EC-B9E3-D4B25D71B9A9}" @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.User EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.UserNovelDataService", "FictionArchive.Service.UserNovelDataService\FictionArchive.Service.UserNovelDataService.csproj", "{A278565B-D440-4AB9-B2E2-41BA3B3AD82A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.ReportingService", "FictionArchive.Service.ReportingService\FictionArchive.Service.ReportingService.csproj", "{24C8871B-1ECA-47A0-92DC-AD155C17EDF0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,5 +75,9 @@ Global {A278565B-D440-4AB9-B2E2-41BA3B3AD82A}.Debug|Any CPU.Build.0 = Debug|Any CPU {A278565B-D440-4AB9-B2E2-41BA3B3AD82A}.Release|Any CPU.ActiveCfg = Release|Any CPU {A278565B-D440-4AB9-B2E2-41BA3B3AD82A}.Release|Any CPU.Build.0 = Release|Any CPU + {24C8871B-1ECA-47A0-92DC-AD155C17EDF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24C8871B-1ECA-47A0-92DC-AD155C17EDF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24C8871B-1ECA-47A0-92DC-AD155C17EDF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24C8871B-1ECA-47A0-92DC-AD155C17EDF0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/docker-compose.yml b/docker-compose.yml index 2078e52..36c1fc0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -136,6 +136,22 @@ services: condition: service_healthy restart: unless-stopped + reporting-service: + build: + context: . + dockerfile: FictionArchive.Service.ReportingService/Dockerfile + networks: + - fictionarchive + environment: + ConnectionStrings__DefaultConnection: Host=postgres;Database=FictionArchive_ReportingService;Username=${POSTGRES_USER:-postgres};Password=${POSTGRES_PASSWORD:-postgres} + RabbitMQ__ConnectionString: amqp://${RABBITMQ_USER:-guest}:${RABBITMQ_PASSWORD:-guest}@rabbitmq + depends_on: + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + restart: unless-stopped + file-service: image: git.orfl.xyz/conco/fictionarchive-file-service:latest networks: