diff --git a/Directory.Packages.props b/Directory.Packages.props index 52b8793..f07f7a1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,16 +1,34 @@ - - net8.0 - enable - enable - 8.0.0 - - - - - - - - - - + + net8.0 + enable + enable + 8.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nocr.TextMatcher.sln b/Nocr.TextMatcher.sln index 3cf1182..fc55740 100644 --- a/Nocr.TextMatcher.sln +++ b/Nocr.TextMatcher.sln @@ -14,6 +14,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nocr.TextMatcher.AppService EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nocr.TextMatcher.Host", "src\Nocr.TextMatcher.Host\Nocr.TextMatcher.Host.csproj", "{58D5C9FD-75A9-4FFB-9FBD-BE8E9FCE3016}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nocr.TextMatcher.Async.Api.Contracts", "src\Nocr.TextMatcher.Async.Api.Contracts\Nocr.TextMatcher.Async.Api.Contracts.csproj", "{4028666B-FAE8-4DB9-8D10-B00F5F31BDCC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nocr.TextMatcher.Api.Contracts", "src\Nocr.TextMatcher.Api.Contracts\Nocr.TextMatcher.Api.Contracts.csproj", "{A6332064-40EE-498A-826D-638BC295A185}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nocr.TextMatcher.AppServices.Contracts", "src\Nocr.TextMatcher.AppServices.Contracts\Nocr.TextMatcher.AppServices.Contracts.csproj", "{01BF99EE-D635-4931-9CAB-B51C54F0760B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Tests", "_Tests", "{6E4D9F75-861F-4C00-A5C8-00D1BEE5A659}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nocr.TextMatcher.AppServices.UnitTests", "tests\Nocr.TextMatcher.AppServices.UnitTests\Nocr.TextMatcher.AppServices.UnitTests.csproj", "{B721E055-84AF-44C6-973D-33241FD2EA7C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,5 +42,24 @@ Global {58D5C9FD-75A9-4FFB-9FBD-BE8E9FCE3016}.Debug|Any CPU.Build.0 = Debug|Any CPU {58D5C9FD-75A9-4FFB-9FBD-BE8E9FCE3016}.Release|Any CPU.ActiveCfg = Release|Any CPU {58D5C9FD-75A9-4FFB-9FBD-BE8E9FCE3016}.Release|Any CPU.Build.0 = Release|Any CPU + {4028666B-FAE8-4DB9-8D10-B00F5F31BDCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4028666B-FAE8-4DB9-8D10-B00F5F31BDCC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4028666B-FAE8-4DB9-8D10-B00F5F31BDCC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4028666B-FAE8-4DB9-8D10-B00F5F31BDCC}.Release|Any CPU.Build.0 = Release|Any CPU + {A6332064-40EE-498A-826D-638BC295A185}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6332064-40EE-498A-826D-638BC295A185}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6332064-40EE-498A-826D-638BC295A185}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6332064-40EE-498A-826D-638BC295A185}.Release|Any CPU.Build.0 = Release|Any CPU + {01BF99EE-D635-4931-9CAB-B51C54F0760B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01BF99EE-D635-4931-9CAB-B51C54F0760B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01BF99EE-D635-4931-9CAB-B51C54F0760B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01BF99EE-D635-4931-9CAB-B51C54F0760B}.Release|Any CPU.Build.0 = Release|Any CPU + {B721E055-84AF-44C6-973D-33241FD2EA7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B721E055-84AF-44C6-973D-33241FD2EA7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B721E055-84AF-44C6-973D-33241FD2EA7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B721E055-84AF-44C6-973D-33241FD2EA7C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B721E055-84AF-44C6-973D-33241FD2EA7C} = {6E4D9F75-861F-4C00-A5C8-00D1BEE5A659} EndGlobalSection EndGlobal diff --git a/Nocr.TextMatcher.sln.DotSettings b/Nocr.TextMatcher.sln.DotSettings new file mode 100644 index 0000000..c901271 --- /dev/null +++ b/Nocr.TextMatcher.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Nocr.TextMatcher.Api.Contracts/Nocr.TextMatcher.Api.Contracts.csproj b/src/Nocr.TextMatcher.Api.Contracts/Nocr.TextMatcher.Api.Contracts.csproj new file mode 100644 index 0000000..d134216 --- /dev/null +++ b/src/Nocr.TextMatcher.Api.Contracts/Nocr.TextMatcher.Api.Contracts.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Nocr.TextMatcher.Api.Contracts/TextMatches/Dto/CreateTextMatchRequest.cs b/src/Nocr.TextMatcher.Api.Contracts/TextMatches/Dto/CreateTextMatchRequest.cs new file mode 100644 index 0000000..d20ac87 --- /dev/null +++ b/src/Nocr.TextMatcher.Api.Contracts/TextMatches/Dto/CreateTextMatchRequest.cs @@ -0,0 +1,21 @@ +using Nocr.TextMatcher.AppServices.Contracts.TextMatches; +using RestEase; + +namespace Nocr.TextMatcher.Api.Contracts.TextMatches.Dto; + +public class CreateTextMatchRequest +{ + public long UserId { get; set; } + + public long ChatId { get; set; } + + public string Template { get; set; } + + public TextMatchRule Rule { get; set; } +} + +[BasePath(WebRoutes.TextMatches.Path)] +public interface ITextMatcherController +{ + Task Create([Body] CreateTextMatchRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.Api.Contracts/WebRoutes.cs b/src/Nocr.TextMatcher.Api.Contracts/WebRoutes.cs new file mode 100644 index 0000000..e44c47a --- /dev/null +++ b/src/Nocr.TextMatcher.Api.Contracts/WebRoutes.cs @@ -0,0 +1,11 @@ +namespace Nocr.TextMatcher.Api.Contracts; + +public static class WebRoutes +{ + public const string BasePath = "/api"; + + public static class TextMatches + { + public const string Path = BasePath + "/" + "text-matches"; + } +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.AppServices.Contracts/Nocr.TextMatcher.AppServices.Contracts.csproj b/src/Nocr.TextMatcher.AppServices.Contracts/Nocr.TextMatcher.AppServices.Contracts.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/src/Nocr.TextMatcher.AppServices.Contracts/Nocr.TextMatcher.AppServices.Contracts.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Nocr.TextMatcher.AppServices.Contracts/TextMatches/TextMatchRule.cs b/src/Nocr.TextMatcher.AppServices.Contracts/TextMatches/TextMatchRule.cs new file mode 100644 index 0000000..98f118e --- /dev/null +++ b/src/Nocr.TextMatcher.AppServices.Contracts/TextMatches/TextMatchRule.cs @@ -0,0 +1,13 @@ +namespace Nocr.TextMatcher.AppServices.Contracts.TextMatches; + +public enum TextMatchRule +{ + // Substring + Full = 1, + + // Any word from list splited by space + AnyWord, + + // All words from list splited by space + AllWords +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.AppServices/Nocr.TextMatcher.AppServices.csproj b/src/Nocr.TextMatcher.AppServices/Nocr.TextMatcher.AppServices.csproj index 6c91e3a..d9e63b7 100644 --- a/src/Nocr.TextMatcher.AppServices/Nocr.TextMatcher.AppServices.csproj +++ b/src/Nocr.TextMatcher.AppServices/Nocr.TextMatcher.AppServices.csproj @@ -2,9 +2,14 @@ + + + + + diff --git a/src/Nocr.TextMatcher.AppServices/ServiceCollectionExtensions.cs b/src/Nocr.TextMatcher.AppServices/ServiceCollectionExtensions.cs index c7a37ee..a6a4fa4 100644 --- a/src/Nocr.TextMatcher.AppServices/ServiceCollectionExtensions.cs +++ b/src/Nocr.TextMatcher.AppServices/ServiceCollectionExtensions.cs @@ -1,4 +1,8 @@ using Microsoft.Extensions.DependencyInjection; +using Nocr.TextMatcher.AppServices.TextMatchers; +using Nocr.TextMatcher.AppServices.TextMatches.Repositories; +using Nocr.TextMatcher.AppServices.TextMatches.Services; +using Rebus.Config; namespace Nocr.TextMatcher.AppServices; @@ -10,6 +14,9 @@ public static class ServiceCollectionExtensions throw new ArgumentNullException(nameof(services)); // Add registrations here + services.AddRebusHandler(); + services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Nocr.TextMatcher.AppServices/TextMatchers/MessageReceivedHandler.cs b/src/Nocr.TextMatcher.AppServices/TextMatchers/MessageReceivedHandler.cs new file mode 100644 index 0000000..6ef020c --- /dev/null +++ b/src/Nocr.TextMatcher.AppServices/TextMatchers/MessageReceivedHandler.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Logging; +using Nocr.TelegramListener.Async.Api.Contracts.Events; +using Nocr.TextMatcher.AppServices.TextMatches.Services; +using Nocr.TextMatcher.Async.Api.Contracts; +using Nocr.TextMatcher.Core.Dates; +using Rebus.Bus; +using Rebus.Handlers; + +namespace Nocr.TextMatcher.AppServices.TextMatchers; + +public sealed class MessageReceivedHandler : IHandleMessages +{ + private readonly ILogger _logger; + private readonly IBus _bus; + private readonly ITextMatchService _textMatchService; + private readonly ICurrentDateProvider _dateProvider; + + public MessageReceivedHandler(ILogger logger, + IBus bus, + ITextMatchService textMatchService, + ICurrentDateProvider dateProvider) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _bus = bus ?? throw new ArgumentNullException(nameof(bus)); + _textMatchService = textMatchService ?? throw new ArgumentNullException(nameof(textMatchService)); + _dateProvider = dateProvider ?? throw new ArgumentNullException(nameof(dateProvider)); + } + + public async Task Handle(MessageReceived message) + { + _logger.LogInformation("Received message: {@Message}", message); + + var matches = await _textMatchService.Get(); + + foreach (var match in matches) + { + if (match.IsMatches(message.ChatId, message.Text)) + { + _logger.LogInformation("Message {@Message} matched {@Match}", message, match); + var @event = new TextMatchMatched + { + ChatId = message.ChatId, + Text = message.Text, + UserId = message.From, + OccuredDateTime = message.OccuredDateTime, + PublishedDateTime = _dateProvider.UtcNow + }; + + await _bus.Advanced.Topics.Publish("nocr.text.matcher", @event); + } + } + } +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.AppServices/TextMatches/Repositories/ITextMatchRepository.cs b/src/Nocr.TextMatcher.AppServices/TextMatches/Repositories/ITextMatchRepository.cs new file mode 100644 index 0000000..33c9a4f --- /dev/null +++ b/src/Nocr.TextMatcher.AppServices/TextMatches/Repositories/ITextMatchRepository.cs @@ -0,0 +1,12 @@ +using Nocr.TextMatcher.AppServices.TextMatches.Services; + +namespace Nocr.TextMatcher.AppServices.TextMatches.Repositories; + +public interface ITextMatchRepository +{ + Task Create(TextMatch textMatch, CancellationToken cancellationToken = default); + + Task Delete(long id, CancellationToken cancellationToken = default); + + Task> Get(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.AppServices/TextMatches/Repositories/InMemoryTextMatchRepository.cs b/src/Nocr.TextMatcher.AppServices/TextMatches/Repositories/InMemoryTextMatchRepository.cs new file mode 100644 index 0000000..81f6284 --- /dev/null +++ b/src/Nocr.TextMatcher.AppServices/TextMatches/Repositories/InMemoryTextMatchRepository.cs @@ -0,0 +1,30 @@ +namespace Nocr.TextMatcher.AppServices.TextMatches.Repositories; + +public sealed class InMemoryTextMatchRepository : ITextMatchRepository +{ + private long _id = 0; + + private List _textMatches = new List(); + + public Task Create(TextMatch textMatch, CancellationToken cancellationToken = default) + { + var id = Interlocked.Increment(ref _id); + textMatch.Id = id; + _textMatches.Add(textMatch); + + return Task.FromResult(id); + } + + public Task Delete(long id, CancellationToken cancellationToken = default) + { + var index = _textMatches.FindIndex(x => x.Id == id); + _textMatches.RemoveAt(index); + + return Task.CompletedTask; + } + + public Task> Get(CancellationToken cancellationToken = default) + { + return Task.FromResult>(_textMatches); + } +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.AppServices/TextMatches/Services/ITextMatchService.cs b/src/Nocr.TextMatcher.AppServices/TextMatches/Services/ITextMatchService.cs new file mode 100644 index 0000000..a6ed2a1 --- /dev/null +++ b/src/Nocr.TextMatcher.AppServices/TextMatches/Services/ITextMatchService.cs @@ -0,0 +1,12 @@ +using Nocr.TextMatcher.AppServices.Contracts.TextMatches; + +namespace Nocr.TextMatcher.AppServices.TextMatches.Services; + +public interface ITextMatchService +{ + Task Create(long userId, long chatId, string template, TextMatchRule rule, CancellationToken cancellationToken = default); + + Task Delete(long id, CancellationToken cancellationToken = default); + + Task> Get(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.AppServices/TextMatches/Services/TextMatchService.cs b/src/Nocr.TextMatcher.AppServices/TextMatches/Services/TextMatchService.cs new file mode 100644 index 0000000..a25aea9 --- /dev/null +++ b/src/Nocr.TextMatcher.AppServices/TextMatches/Services/TextMatchService.cs @@ -0,0 +1,48 @@ +using Nocr.TextMatcher.AppServices.Contracts.TextMatches; +using Nocr.TextMatcher.AppServices.TextMatches.Repositories; +using Nocr.TextMatcher.Async.Api.Contracts; +using Nocr.TextMatcher.Core.Dates; +using Rebus.Bus; + +namespace Nocr.TextMatcher.AppServices.TextMatches.Services; + +public sealed class TextMatchService : ITextMatchService +{ + private readonly IBus _bus; + private readonly ITextMatchRepository _repository; + private readonly ICurrentDateProvider _dateProvider; + + public TextMatchService(IBus bus, ITextMatchRepository repository, + ICurrentDateProvider dateProvider) + { + _bus = bus ?? throw new ArgumentNullException(nameof(bus)); + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _dateProvider = dateProvider ?? throw new ArgumentNullException(nameof(dateProvider)); + } + + public async Task Create(long userId, long chatId, string template, TextMatchRule rule, + CancellationToken cancellationToken = default) + { + var textMatch = TextMatch.Initialize(userId, chatId, template, rule, _dateProvider.UtcNow); + await _repository.Create(textMatch, cancellationToken); + + var @event = new TextMatchCreated + { + ChatId = chatId + }; + + await _bus.Advanced.Topics.Publish("nocr.text.matcher", @event); + + return textMatch.Id; + } + + public Task Delete(long id, CancellationToken cancellationToken = default) + { + return _repository.Delete(id, cancellationToken); + } + + public Task> Get(CancellationToken cancellationToken = default) + { + return _repository.Get(cancellationToken); + } +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.AppServices/TextMatches/TextMatch.cs b/src/Nocr.TextMatcher.AppServices/TextMatches/TextMatch.cs new file mode 100644 index 0000000..7744169 --- /dev/null +++ b/src/Nocr.TextMatcher.AppServices/TextMatches/TextMatch.cs @@ -0,0 +1,70 @@ +using System.Text.RegularExpressions; +using Nocr.TextMatcher.AppServices.Contracts.TextMatches; + +namespace Nocr.TextMatcher.AppServices.TextMatches; + +public sealed class TextMatch +{ + public long Id { get; set; } + + public long UserId { get; private set; } + + public long ChatId { get; private set; } + + public string Template { get; private set; } + + public TextMatchRule Rule { get; private set; } + + public DateTimeOffset CreatedDateTime { get; private set; } + + private TextMatch(long userId, + long chatId, + string template, + TextMatchRule rule, + DateTimeOffset createdDateTime) + { + UserId = userId; + ChatId = chatId; + Template = template; + Rule = rule; + CreatedDateTime = createdDateTime; + } + + public static TextMatch Initialize(long userId, + long chatId, + string template, + TextMatchRule rule, + DateTimeOffset createdDateTime) + { + if (userId <= 0) + throw new ArgumentException("User id should be greater tha 0", nameof(userId)); + + if (chatId <= 0) + throw new ArgumentException("Chat id should be greater tha 0", nameof(chatId)); + + if (string.IsNullOrWhiteSpace(template)) + throw new ArgumentException("Template should not be empty", nameof(template)); + + return new TextMatch(userId, chatId, template, rule, createdDateTime); + } + + public bool IsMatches(long chatId, string text) + { + if (ChatId != chatId) + return false; + + switch (Rule) + { + case TextMatchRule.Full: + return text.Contains(Template, StringComparison.OrdinalIgnoreCase); + case TextMatchRule.AnyWord: + var anyWords = Regex.Split(Template, @"\s+"); + return anyWords.Any(word => text.Contains(word, StringComparison.OrdinalIgnoreCase)); + case TextMatchRule.AllWords: + var allWords = Regex.Split(Template, @"\s+"); + return allWords.All(word => text.Contains(word, StringComparison.OrdinalIgnoreCase)); + default: + throw new ArgumentOutOfRangeException(nameof(Rule)); + } + } +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.Async.Api.Contracts/IEvent.cs b/src/Nocr.TextMatcher.Async.Api.Contracts/IEvent.cs new file mode 100644 index 0000000..1839c74 --- /dev/null +++ b/src/Nocr.TextMatcher.Async.Api.Contracts/IEvent.cs @@ -0,0 +1,6 @@ +namespace Nocr.TextMatcher.Async.Api.Contracts; + +public interface IEvent +{ + public Guid Id { get; } +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.Async.Api.Contracts/Nocr.TextMatcher.Async.Api.Contracts.csproj b/src/Nocr.TextMatcher.Async.Api.Contracts/Nocr.TextMatcher.Async.Api.Contracts.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/src/Nocr.TextMatcher.Async.Api.Contracts/Nocr.TextMatcher.Async.Api.Contracts.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Nocr.TextMatcher.Async.Api.Contracts/TextMatchCreated.cs b/src/Nocr.TextMatcher.Async.Api.Contracts/TextMatchCreated.cs new file mode 100644 index 0000000..6544e5b --- /dev/null +++ b/src/Nocr.TextMatcher.Async.Api.Contracts/TextMatchCreated.cs @@ -0,0 +1,8 @@ +namespace Nocr.TextMatcher.Async.Api.Contracts; + +public class TextMatchCreated : IEvent +{ + public Guid Id => Guid.NewGuid(); + + public long ChatId { get; set; } +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.Async.Api.Contracts/TextMatchMatched.cs b/src/Nocr.TextMatcher.Async.Api.Contracts/TextMatchMatched.cs new file mode 100644 index 0000000..4a58507 --- /dev/null +++ b/src/Nocr.TextMatcher.Async.Api.Contracts/TextMatchMatched.cs @@ -0,0 +1,16 @@ +namespace Nocr.TextMatcher.Async.Api.Contracts; + +public class TextMatchMatched : IEvent +{ + public Guid Id => Guid.NewGuid(); + + public long UserId { get; set; } + + public long ChatId { get; set; } + + public string Text { get; set; } + + public DateTimeOffset OccuredDateTime { get; set; } + + public DateTimeOffset PublishedDateTime { get; set; } +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.Core/Options/RebusRabbitMqOptions.cs b/src/Nocr.TextMatcher.Core/Options/RebusRabbitMqOptions.cs new file mode 100644 index 0000000..2894977 --- /dev/null +++ b/src/Nocr.TextMatcher.Core/Options/RebusRabbitMqOptions.cs @@ -0,0 +1,12 @@ +namespace Nocr.TextMatcher.Core.Options; + +public sealed class RebusRabbitMqOptions +{ + public string ConnectionString { get; set; } + + public string InputQueueName { get; set; } + + public string DirectExchangeName { get; set; } + + public string TopicsExchangeName { get; set; } +} \ No newline at end of file diff --git a/src/Nocr.TextMatcher.Host/Controllers/TextMatchController.cs b/src/Nocr.TextMatcher.Host/Controllers/TextMatchController.cs new file mode 100644 index 0000000..509388c --- /dev/null +++ b/src/Nocr.TextMatcher.Host/Controllers/TextMatchController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Nocr.TextMatcher.Api.Contracts; +using Nocr.TextMatcher.Api.Contracts.TextMatches; +using Nocr.TextMatcher.Api.Contracts.TextMatches.Dto; +using Nocr.TextMatcher.AppServices.TextMatches; +using Nocr.TextMatcher.AppServices.TextMatches.Services; + +namespace Nocr.TextMatcher.Host.Controllers; + +[ApiController] +[Route(WebRoutes.TextMatches.Path)] +public class TextMatchController : ControllerBase +{ + private readonly ITextMatchService _textMatchService; + + public TextMatchController(ITextMatchService textMatchService) + { + _textMatchService = textMatchService ?? throw new ArgumentNullException(nameof(textMatchService)); + } + + [HttpPost] + public Task Create([FromBody] CreateTextMatchRequest request, CancellationToken cancellationToken = default) + { + return _textMatchService.Create(request.UserId, request.ChatId, request.Template, request.Rule, + cancellationToken); + } +} diff --git a/src/Nocr.TextMatcher.Host/Infrastructure/HostBuilderFactory.cs b/src/Nocr.TextMatcher.Host/Infrastructure/HostBuilderFactory.cs index 60cd770..fa80ff8 100644 --- a/src/Nocr.TextMatcher.Host/Infrastructure/HostBuilderFactory.cs +++ b/src/Nocr.TextMatcher.Host/Infrastructure/HostBuilderFactory.cs @@ -12,7 +12,7 @@ public class HostBuilderFactory where TStartup : class if (!string.IsNullOrWhiteSpace(baseDirectory)) configurationBuilder.SetBasePath(baseDirectory); - configurationBuilder.AddJsonFile("appsettings.protected.json", false); + configurationBuilder.AddJsonFile("appsettings.protected.json", true); }) .ConfigureWebHostDefaults(host => { host.UseStartup(); }) .UseSerilog((ctx, logBuilder) => diff --git a/src/Nocr.TextMatcher.Host/Infrastructure/Startup.cs b/src/Nocr.TextMatcher.Host/Infrastructure/Startup.cs index 5c4d568..9ce51d3 100644 --- a/src/Nocr.TextMatcher.Host/Infrastructure/Startup.cs +++ b/src/Nocr.TextMatcher.Host/Infrastructure/Startup.cs @@ -1,5 +1,11 @@ +using Microsoft.Extensions.Options; using Nocr.TextMatcher.AppServices; using Nocr.TextMatcher.Core.Dates; +using Nocr.TextMatcher.Core.Options; +using Rebus.Bus; +using Rebus.Config; +using Rebus.Routing.TypeBased; +using Rebus.Serialization.Json; namespace Nocr.TextMatcher.Host.Infrastructure; @@ -16,10 +22,39 @@ public class Startup { services.AddSingleton(); + services.AddControllers(); + + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + services.AddAppServices(); + + services.Configure(Configuration.GetSection(nameof(RebusRabbitMqOptions))); + services.AddRebus((builder, ctx) => + builder.Transport(t => + { + var rebusOptions = ctx.GetRequiredService>().Value; + t.UseRabbitMq(rebusOptions.ConnectionString, rebusOptions.InputQueueName) + .DefaultQueueOptions(queue => queue.SetDurable(true)) + .ExchangeNames(rebusOptions.DirectExchangeName, rebusOptions.TopicsExchangeName); + }) + .Serialization(s => s.UseSystemTextJson()) + .Logging(l => l.Serilog()) + .Routing(r => r.TypeBased())); } - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + if (!env.IsProduction()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseRouting(); + app.UseEndpoints(builder => builder.MapControllers()); + + var bus = app.ApplicationServices.GetRequiredService(); + bus.Advanced.Topics.Subscribe("nocr.telegram.listener").GetAwaiter().GetResult(); } } \ No newline at end of file diff --git a/src/Nocr.TextMatcher.Host/Nocr.TextMatcher.Host.csproj b/src/Nocr.TextMatcher.Host/Nocr.TextMatcher.Host.csproj index dc47084..b276427 100644 --- a/src/Nocr.TextMatcher.Host/Nocr.TextMatcher.Host.csproj +++ b/src/Nocr.TextMatcher.Host/Nocr.TextMatcher.Host.csproj @@ -6,9 +6,18 @@ + + + + + + + + + diff --git a/src/Nocr.TextMatcher.Host/appsettings.Development.json b/src/Nocr.TextMatcher.Host/appsettings.Development.json index fdf6b48..2d26fac 100644 --- a/src/Nocr.TextMatcher.Host/appsettings.Development.json +++ b/src/Nocr.TextMatcher.Host/appsettings.Development.json @@ -8,5 +8,8 @@ } } ] + }, + "RebusRabbitMqOptions": { + "ConnectionString": "amqp://admin:admin@localhost:5672/" } } diff --git a/src/Nocr.TextMatcher.Host/appsettings.DockerCompose.json b/src/Nocr.TextMatcher.Host/appsettings.DockerCompose.json new file mode 100644 index 0000000..d3c47a6 --- /dev/null +++ b/src/Nocr.TextMatcher.Host/appsettings.DockerCompose.json @@ -0,0 +1,28 @@ +{ + "Serilog": { + "MinimumLevel": { + "Default": "Information" + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Level:u3}] {Timestamp:MM-dd HH:mm:ss} {TraceId} {SourceContext:l} {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "/var/log/nocr/text-matcher/nocr-text-matcher-.log", + "outputTemplate": "[{Level:u3}] {Timestamp:dd-MM-yyyy HH:mm:ss} {TraceId} {SourceContext:l} {Message:lj}{NewLine}{Exception}", + "fileSizeLimitBytes": 104857600, + "rollingInterval": "Day", + "rollOnFileSizeLimit": true + } + } + ] + }, + "RebusRabbitMqOptions": { + "ConnectionString": "amqp://admin:admin@nocr-rabbitmq:5672/" + } +} diff --git a/src/Nocr.TextMatcher.Host/appsettings.Production.json b/src/Nocr.TextMatcher.Host/appsettings.Production.json index d0a7c99..d3c47a6 100644 --- a/src/Nocr.TextMatcher.Host/appsettings.Production.json +++ b/src/Nocr.TextMatcher.Host/appsettings.Production.json @@ -21,5 +21,8 @@ } } ] + }, + "RebusRabbitMqOptions": { + "ConnectionString": "amqp://admin:admin@nocr-rabbitmq:5672/" } } diff --git a/src/Nocr.TextMatcher.Host/appsettings.json b/src/Nocr.TextMatcher.Host/appsettings.json index ff9ad20..fd2a17d 100644 --- a/src/Nocr.TextMatcher.Host/appsettings.json +++ b/src/Nocr.TextMatcher.Host/appsettings.json @@ -5,8 +5,14 @@ "Override": { "Microsoft": "Information", "Microsoft.AspNetCore": "Error", - "System.Net.Http.HttpClient": "Warning" + "System.Net.Http.HttpClient": "Warning", + "Rebus": "Warning" } } + }, + "RebusRabbitMqOptions": { + "InputQueueName": "nocr.text.matcher.queue", + "DirectExchangeName": "nocr.direct", + "TopicsExchangeName": "nocr.topics" } } diff --git a/tests/Nocr.TextMatcher.AppServices.UnitTests/Nocr.TextMatcher.AppServices.UnitTests.csproj b/tests/Nocr.TextMatcher.AppServices.UnitTests/Nocr.TextMatcher.AppServices.UnitTests.csproj new file mode 100644 index 0000000..a39f68b --- /dev/null +++ b/tests/Nocr.TextMatcher.AppServices.UnitTests/Nocr.TextMatcher.AppServices.UnitTests.csproj @@ -0,0 +1,25 @@ + + + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/Nocr.TextMatcher.AppServices.UnitTests/TextMatchTests.cs b/tests/Nocr.TextMatcher.AppServices.UnitTests/TextMatchTests.cs new file mode 100644 index 0000000..2a2e3f7 --- /dev/null +++ b/tests/Nocr.TextMatcher.AppServices.UnitTests/TextMatchTests.cs @@ -0,0 +1,68 @@ +using Nocr.TextMatcher.AppServices.Contracts.TextMatches; +using Nocr.TextMatcher.AppServices.TextMatches; +using Xunit; + +namespace Nocr.TextMatcher.AppServices.UnitTests; + +public class TextMatchTests +{ + private const long UserId = 1; + private const long ChatId = 1; + + private static DateTimeOffset CreatedDateTime = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); + + [Fact] + public void IsMatches_SameChatId_FullRule_MatchesText() + { + // Arrange + var match = TextMatch.Initialize(UserId, ChatId, "велосипед", TextMatchRule.Full, CreatedDateTime); + var text = "Продам снежный велосипед 100 лари. Гудаури."; + + // Act + var result = match.IsMatches(ChatId, text); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsMatches_SameChatId_AnyWord_MatchesText() + { + // Arrange + var match = TextMatch.Initialize(UserId, ChatId, "iphone айфон", TextMatchRule.AnyWord, CreatedDateTime); + var text = "Продам айфон велосипед 100 лари. Гудаури."; + + // Act + var result = match.IsMatches(ChatId, text); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsMatches_SameChatId_AllWords_MatchesText() + { + // Arrange + var match = TextMatch.Initialize(UserId, ChatId, "iphone айфон", TextMatchRule.AnyWord, CreatedDateTime); + var text = "Гомарджоба. Продам iphone (айфон) 1000 лари. Гудаури."; + + // Act + var result = match.IsMatches(ChatId, text); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsMatches_DifferentChatId_NotMatchesText() + { + // Arrange + var match = TextMatch.Initialize(UserId, ChatId, "iphone", TextMatchRule.Full, CreatedDateTime); + + // Act + var result = match.IsMatches(ChatId, string.Empty); + + // Assert + Assert.False(result); + } +} \ No newline at end of file