Add subscribe command

This commit is contained in:
Sergey Nazarov 2024-03-26 15:42:59 +03:00
parent 33074e99a0
commit 658b28799a
16 changed files with 287 additions and 42 deletions

View File

@ -1,31 +1,34 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<MicrosoftVersion>8.0.0</MicrosoftVersion> <MicrosoftVersion>8.0.0</MicrosoftVersion>
<InsightTelegramBotVersion>0.16.0</InsightTelegramBotVersion> <InsightTelegramBotVersion>0.16.0</InsightTelegramBotVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Nocr"> <ItemGroup Label="Nocr">
<PackageVersion Include="Nocr.TextMatcher.Async.Api.Contracts" Version="0.4.7" /> <PackageVersion Include="Nocr.TextMatcher.Api.Contracts" Version="0.4.13" />
</ItemGroup> <PackageVersion Include="Nocr.TextMatcher.Async.Api.Contracts" Version="0.4.13" />
<ItemGroup Label="Rebus"> <PackageVersion Include="Nocr.Users.Api.Contracts" Version="0.4.11" />
<PackageVersion Include="Rebus" Version="8.2.2"/> </ItemGroup>
<PackageVersion Include="Rebus.ServiceProvider" Version="10.1.0"/> <ItemGroup Label="Rebus">
<PackageVersion Include="Rebus.RabbitMq" Version="9.0.1"/> <PackageVersion Include="Rebus" Version="8.2.2" />
<PackageVersion Include="Rebus.Serilog" Version="8.0.0"/> <PackageVersion Include="Rebus.ServiceProvider" Version="10.1.0" />
</ItemGroup> <PackageVersion Include="Rebus.RabbitMq" Version="9.0.1" />
<ItemGroup Label="Insight.TelegramBot"> <PackageVersion Include="Rebus.Serilog" Version="8.0.0" />
<PackageVersion Include="Insight.TelegramBot" Version="$(InsightTelegramBotVersion)"/> </ItemGroup>
<PackageVersion Include="Insight.TelegramBot.Hosting.DependencyInjection" Version="$(InsightTelegramBotVersion)"/> <ItemGroup Label="Insight.TelegramBot">
<PackageVersion Include="Insight.TelegramBot.Handling" Version="$(InsightTelegramBotVersion)"/> <PackageVersion Include="Insight.TelegramBot" Version="$(InsightTelegramBotVersion)" />
</ItemGroup> <PackageVersion Include="Insight.TelegramBot.Hosting.DependencyInjection" Version="$(InsightTelegramBotVersion)" />
<ItemGroup Label="Serilog"> <PackageVersion Include="Insight.TelegramBot.Handling" Version="$(InsightTelegramBotVersion)" />
<PackageVersion Include="Serilog" Version="3.1.1"/> </ItemGroup>
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1"/> <ItemGroup Label="Serilog">
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0"/> <PackageVersion Include="Serilog" Version="3.1.1" />
</ItemGroup> <PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
<ItemGroup Label="Microsoft"> <PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftVersion)"/> </ItemGroup>
</ItemGroup> <ItemGroup Label="Microsoft">
</Project> <PackageVersion Include="Microsoft.Extensions.Http" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftVersion)" />
</ItemGroup>
</Project>

View File

@ -4,7 +4,7 @@ using Insight.TelegramBot.Models;
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
namespace Nocr.TelegramClient.AppServices.Handlers.Messages; namespace Nocr.TelegramClient.AppServices.Handlers.Messages.StartMesage;
public class StartMessageHandler : IMatchingUpdateHandler<StartMessageMatcher> public class StartMessageHandler : IMatchingUpdateHandler<StartMessageMatcher>
{ {
@ -14,10 +14,11 @@ public class StartMessageHandler : IMatchingUpdateHandler<StartMessageMatcher>
{ {
_bot = bot ?? throw new ArgumentNullException(nameof(bot)); _bot = bot ?? throw new ArgumentNullException(nameof(bot));
} }
public Task Handle(Update update, CancellationToken cancellationToken = default) public Task Handle(Update update, CancellationToken cancellationToken = default)
{ {
var message = new TextMessage(update.Message.Chat) var telegramId = update.Message.From.Id;
var message = new TextMessage(telegramId)
{ {
Text = "Привет! Я _bot_name_", Text = "Привет! Я _bot_name_",
ParseMode = ParseMode.Html ParseMode = ParseMode.Html

View File

@ -1,6 +1,6 @@
using Insight.TelegramBot.Handling.Matchers.TextMatchers; using Insight.TelegramBot.Handling.Matchers.TextMatchers;
namespace Nocr.TelegramClient.AppServices.Handlers.Messages; namespace Nocr.TelegramClient.AppServices.Handlers.Messages.StartMesage;
public sealed class StartMessageMatcher : TextStartWithUpdateMatcher public sealed class StartMessageMatcher : TextStartWithUpdateMatcher
{ {

View File

@ -0,0 +1,74 @@
using System.Text.RegularExpressions;
using Insight.TelegramBot;
using Insight.TelegramBot.Handling.Handlers;
using Insight.TelegramBot.Handling.Matchers.TextMatchers;
using Insight.TelegramBot.Models;
using Nocr.TelegramClient.AppServices.Users;
using Nocr.TextMatcher.Api.Contracts.TextMatches;
using Nocr.TextMatcher.Api.Contracts.TextMatches.Requests;
using Telegram.Bot.Types;
namespace Nocr.TelegramClient.AppServices.Handlers.Messages.SubscribeMessage;
public sealed class SubscribeMessageMatcher : TextStartWithUpdateMatcher
{
public SubscribeMessageMatcher()
{
Template = "/subscribe";
}
}
public class SubscribeMessageHandler : IMatchingUpdateHandler<SubscribeMessageMatcher>
{
private readonly IBot _bot;
private readonly IUsersService _usersService;
private readonly ITextMatchesController _textMatchesController;
/// <summary>
/// Regex to match command "/subscribe @username match_type keywords". <br/>
/// For instance, "/subscribe @baraholka 1 обувь ботинки сапоги" will create match for a current user with type "All" and pattern "обувь ботинки сапоги".
/// </summary>
private static Regex _commandRegex =
new Regex(@"/subscribe (.*\B@(?=\w{5,32}\b)[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*.*) (\d{1}) (.*)",
RegexOptions.Compiled);
public SubscribeMessageHandler(IBot bot, IUsersService usersService, ITextMatchesController textMatchesController)
{
_bot = bot ?? throw new ArgumentNullException(nameof(bot));
_usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
_textMatchesController =
textMatchesController ?? throw new ArgumentNullException(nameof(textMatchesController));
}
public async Task Handle(Update update, CancellationToken cancellationToken = default)
{
var telegramId = update.Message.From.Id;
var match = _commandRegex.Match(update.Message.Text);
if (!match.Success)
{
await _bot.SendMessageAsync(new TextMessage(telegramId)
{
Text = "Команда не удовлетворяет формату"
}, CancellationToken.None);
}
var username = match.Groups[1].Value.TrimStart('@');
var rule = match.Groups[2].Value;
var template = match.Groups[3].Value;
var user = await _usersService.GetOrCreate(telegramId, update.Message.From.Username, cancellationToken);
var matchId = await _textMatchesController.Create(new CreateTextMatchRequest
{
UserId = user.Id,
ChatUsername = username,
Rule = (TextMatchRule)Convert.ToInt32(rule),
Template = template,
}, cancellationToken);
await _bot.SendMessageAsync(new TextMessage(telegramId)
{
Text = $"Подписка создана: {matchId}"
}, CancellationToken.None);
}
}

View File

@ -1,26 +1,74 @@
using Insight.TelegramBot; using Insight.TelegramBot;
using Insight.TelegramBot.Models; using Insight.TelegramBot.Models;
using Microsoft.Extensions.Logging;
using Nocr.TelegramClient.AppServices.Users;
using Nocr.TextMatcher.Api.Contracts.TextMatches;
using Nocr.TextMatcher.Async.Api.Contracts; using Nocr.TextMatcher.Async.Api.Contracts;
using Nocr.Users.AppServices.Contracts.Users;
using Rebus.Handlers; using Rebus.Handlers;
namespace Nocr.TelegramClient.AppServices.Matches; namespace Nocr.TelegramClient.AppServices.Matches;
public class TextMatchMatchedHandler : IHandleMessages<TextMatchMatched> public class TextMatchMatchedHandler : IHandleMessages<TextMatchMatched>
{ {
private readonly ILogger<TextMatchMatchedHandler> _logger;
private readonly IBot _bot; private readonly IBot _bot;
private readonly ITextMatchesController _textMatchesController;
private readonly IUsersService _usersService;
public TextMatchMatchedHandler(IBot bot) public TextMatchMatchedHandler(ILogger<TextMatchMatchedHandler> logger, IBot bot,
ITextMatchesController textMatchesController, IUsersService usersService)
{ {
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_bot = bot ?? throw new ArgumentNullException(nameof(bot)); _bot = bot ?? throw new ArgumentNullException(nameof(bot));
_textMatchesController =
textMatchesController ?? throw new ArgumentNullException(nameof(textMatchesController));
_usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
} }
public async Task Handle(TextMatchMatched message) public async Task Handle(TextMatchMatched message)
{ {
var textMessage = new TextMessage(58874635) var match = await _textMatchesController.GetById(message.MatchId);
if (match == null)
{ {
Text = $"[{message.PublishedDateTime:MM.dd.yyyy HH:mm:ss}] Найдено совпадение.\n @{message.ChatUsername} > {message.Text}" _logger.LogWarning("Match [{MatchId}] from message {MessageId} not found", message.MatchId, message.Id);
return;
}
var user = await _usersService.GetById(message.MatchUserId);
if (user == null)
{
_logger.LogWarning("User [{UserId}] of [{MatchId}] from message {MessageId} not found", message.MatchUserId, message.MatchId, message.Id);
return;
}
var identity = user.Identities.FirstOrDefault(x => x.Type == UserIdentityType.TelegramId);
if (identity == null)
{
_logger.LogWarning("There is no identity with type '{IdentityType}' for user '{UserId}'", UserIdentityType.TelegramId, user.Id);
return;
}
var textMessage = new TextMessage(long.Parse(identity.Identity))
{
Text = $"[{message.PublishedDateTime:MM.dd.yyyy HH:mm:ss}] Найдено совпадение.\n @{match.ChatUsername}. Тип совпадения: '{GetTextMatchRule(match.Rule)}', шаблон: '{match.Template}'"
}; };
await _bot.SendMessageAsync(textMessage); await _bot.SendMessageAsync(textMessage);
} }
private string GetTextMatchRule(TextMatchRule rule)
{
switch (rule)
{
case TextMatchRule.Full:
return "Полное";
case TextMatchRule.AllWords:
return "Все слова из списка";
case TextMatchRule.AnyWord:
return "Одно слово из списка";
default:
throw new IndexOutOfRangeException(nameof(rule));
}
}
} }

View File

@ -6,10 +6,16 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Insight.TelegramBot" /> <PackageReference Include="Insight.TelegramBot" />
<PackageReference Include="Insight.TelegramBot.Handling" /> <PackageReference Include="Insight.TelegramBot.Handling" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Nocr.Users.Api.Contracts" />
<PackageReference Include="Rebus" /> <PackageReference Include="Rebus" />
<PackageReference Include="Rebus.ServiceProvider" /> <PackageReference Include="Rebus.ServiceProvider" />
<PackageReference Include="Nocr.TextMatcher.Async.Api.Contracts" /> <PackageReference Include="Nocr.TextMatcher.Async.Api.Contracts" />
<PackageReference Include="Nocr.TextMatcher.Api.Contracts" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nocr.TelegramClient.Core\Nocr.TelegramClient.Core.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,7 @@
using Nocr.TelegramClient.Core.Options;
namespace Nocr.TelegramClient.AppServices.Options;
public class TextMatcherRestEaseOptions : RestEaseOptions
{
}

View File

@ -0,0 +1,7 @@
using Nocr.TelegramClient.Core.Options;
namespace Nocr.TelegramClient.AppServices.Options;
public class UsersRestEaseOptions : RestEaseOptions
{
}

View File

@ -1,18 +1,50 @@
using System.Resources;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Nocr.TelegramClient.AppServices.Matches; using Nocr.TelegramClient.AppServices.Matches;
using Nocr.TelegramClient.AppServices.Options;
using Nocr.TelegramClient.AppServices.Users;
using Nocr.TelegramClient.Core.Options;
using Nocr.TextMatcher.Api.Contracts.TextMatches;
using Nocr.Users.Api.Contracts.Users;
using Rebus.Config; using Rebus.Config;
using RestEase;
namespace Nocr.TelegramClient.AppServices; namespace Nocr.TelegramClient.AppServices;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddAppServices(this IServiceCollection services) public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
{ {
if (services == null) if (services == null)
throw new ArgumentNullException(nameof(services)); throw new ArgumentNullException(nameof(services));
// Add registrations here // Add registrations here
services.AddRebusHandler<TextMatchMatchedHandler>(); services.AddRebusHandler<TextMatchMatchedHandler>();
services.AddHttpClient();
services.Configure<UsersRestEaseOptions>(configuration.GetSection(nameof(UsersRestEaseOptions)));
services.AddScoped<IUsersController>(ctx =>
{
var options = ctx.GetRequiredService<IOptions<UsersRestEaseOptions>>().Value;
var httpClientFactory = ctx.GetRequiredService<IHttpClientFactory>();
var client = httpClientFactory.CreateClient(nameof(IUsersController));
client.BaseAddress = new Uri(options.BasePath);
return RestClient.For<IUsersController>(client);
});
services.Configure<TextMatcherRestEaseOptions>(configuration.GetSection(nameof(TextMatcherRestEaseOptions)));
services.AddScoped<ITextMatchesController>(ctx =>
{
var options = ctx.GetRequiredService<IOptions<TextMatcherRestEaseOptions>>().Value;
var httpClientFactory = ctx.GetRequiredService<IHttpClientFactory>();
var client = httpClientFactory.CreateClient(nameof(ITextMatchesController));
client.BaseAddress = new Uri(options.BasePath);
return RestClient.For<ITextMatchesController>(client);
});
services.AddScoped<IUsersService, UsersService>();
return services; return services;
} }

View File

@ -0,0 +1,10 @@
using Nocr.Users.Api.Contracts.Users.Dto;
namespace Nocr.TelegramClient.AppServices.Users;
public interface IUsersService
{
public Task<UserData?> GetOrCreate(long telegramId, string? username, CancellationToken cancellationToken = default);
public Task<UserData?> GetById(long id, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,40 @@
using Nocr.Users.Api.Contracts.Users;
using Nocr.Users.Api.Contracts.Users.Dto;
using Nocr.Users.Api.Contracts.Users.Dto.Requests;
using Nocr.Users.AppServices.Contracts.Users;
namespace Nocr.TelegramClient.AppServices.Users;
public sealed class UsersService : IUsersService
{
private readonly IUsersController _usersController;
public UsersService(IUsersController usersController)
{
_usersController = usersController ?? throw new ArgumentNullException(nameof(usersController));
}
public async Task<UserData?> GetOrCreate(long telegramId, string? username, CancellationToken cancellationToken = default)
{
var user = await _usersController.GetByIdentity(UserIdentityType.TelegramId, telegramId.ToString(), cancellationToken);
if (user == null)
{
await _usersController.Create(new CreateUserRequest
{
Username = username ?? $"User: {telegramId.ToString()}",
TelegramId = telegramId,
TelegramUsername = username
}, cancellationToken);
user = await _usersController.GetByIdentity(UserIdentityType.TelegramId, telegramId.ToString(),
cancellationToken);
}
return user;
}
public Task<UserData?> GetById(long id, CancellationToken cancellationToken = default)
{
return _usersController.GetById(id, cancellationToken);
}
}

View File

@ -0,0 +1,6 @@
namespace Nocr.TelegramClient.Core.Options;
public class RestEaseOptions
{
public string BasePath { get; set; }
}

View File

@ -25,7 +25,6 @@ public class Startup
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddSingleton<ICurrentDateProvider, DefaultCurrentDateProvider>(); services.AddSingleton<ICurrentDateProvider, DefaultCurrentDateProvider>();
services.AddTelegramBot(bot => services.AddTelegramBot(bot =>
bot.WithBot<BotClient>(ServiceLifetime.Transient) bot.WithBot<BotClient>(ServiceLifetime.Transient)
.WithTelegramBotClient(client => client .WithTelegramBotClient(client => client
@ -34,7 +33,8 @@ public class Startup
.WithOptions(opt => opt.FromConfiguration(Configuration)) .WithOptions(opt => opt.FromConfiguration(Configuration))
.WithPolling(polling => polling.WithExceptionHandler<LoggingPollingExceptionHandler>())); .WithPolling(polling => polling.WithExceptionHandler<LoggingPollingExceptionHandler>()));
services.AddTelegramBotHandling(typeof(BotClient).Assembly); services.AddTelegramBotHandling(typeof(BotClient).Assembly);
services.AddAppServices();
services.AddAppServices(Configuration);
services.Configure<RebusRabbitMqOptions>(Configuration.GetSection(nameof(RebusRabbitMqOptions))); services.Configure<RebusRabbitMqOptions>(Configuration.GetSection(nameof(RebusRabbitMqOptions)));
services.AddRebus((builder, ctx) => services.AddRebus((builder, ctx) =>

View File

@ -16,7 +16,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Nocr.TelegramClient.AppServices\Nocr.TelegramClient.AppServices.csproj" /> <ProjectReference Include="..\Nocr.TelegramClient.AppServices\Nocr.TelegramClient.AppServices.csproj" />
<ProjectReference Include="..\Nocr.TelegramClient.Core\Nocr.TelegramClient.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -11,5 +11,11 @@
}, },
"RebusRabbitMqOptions": { "RebusRabbitMqOptions": {
"ConnectionString": "amqp://admin:admin@localhost:5672/" "ConnectionString": "amqp://admin:admin@localhost:5672/"
},
"UsersRestEaseOptions": {
"BasePath": "http://localhost:4998"
},
"TextMatcherRestEaseOptions": {
"BasePath": "http://localhost:5001"
} }
} }

View File

@ -1,5 +1,11 @@
{ {
"RebusRabbitMqOptions": { "RebusRabbitMqOptions": {
"ConnectionString": "amqp://admin:admin@nocr-rabbitmq:5672/" "ConnectionString": "amqp://admin:admin@nocr-rabbitmq:5672/"
},
"UsersRestEaseOptions": {
"BasePath": "http://nocr-users:80"
},
"TextMatcherRestEaseOptions": {
"BasePath": "http://nocr-text-matcher:80"
} }
} }