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