nazarovsa/main_menu (#8)

Reviewed-on: #8
Co-authored-by: Sergey Nazarov <insight.appdev@gmail.com>
Co-committed-by: Sergey Nazarov <insight.appdev@gmail.com>
This commit is contained in:
Sergey Nazarov 2024-04-19 19:43:07 +00:00 committed by nazarovsa
parent 581492a1da
commit 3a91d41443
20 changed files with 203 additions and 115 deletions

View File

@ -23,8 +23,10 @@ public class NocrCallbackData : CallbackData<NocrState>
return new NocrCallbackData(NocrState.DeleteSubscription, subscriptionId.ToString());
}
public static NocrCallbackData ViewSubscription(long subscriptionId)
public static NocrCallbackData ViewSubscription(long? subscriptionId = null)
{
return new NocrCallbackData(NocrState.ViewSubscription, subscriptionId.ToString());
return subscriptionId.HasValue ?
new NocrCallbackData(NocrState.ViewSubscription, subscriptionId.Value.ToString()):
new NocrCallbackData(NocrState.ViewSubscription);
}
}

View File

@ -4,9 +4,10 @@ public enum NocrState
{
// Commands
ActivateSubscription = 1,
DeactivateSubscription = 2,
DeleteSubscription = 3,
DeactivateSubscription,
DeleteSubscription,
// States
ViewSubscription = 4
Start,
ViewSubscription
}

View File

@ -0,0 +1,69 @@
using Insight.Localizer;
using Insight.TelegramBot;
using Insight.TelegramBot.Keyboards;
using Insight.TelegramBot.Models;
using Nocr.TelegramClient.AppServices.Bots;
using Nocr.TelegramClient.AppServices.Bots.MessageDispatcher;
using Nocr.TelegramClient.AppServices.Handlers.Messages.StartMessage;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups;
namespace Nocr.TelegramClient.AppServices.Handlers.BaseHandlers;
public abstract class StartHandlerBase
{
protected IBot Bot { get; }
protected ILocalizer Localizer { get; }
protected IMessageDispatcherQueue MessageQueue { get; }
public StartHandlerBase(ILocalizer localizer, IMessageDispatcherQueue messageQueue, IBot bot)
{
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
Localizer = localizer ?? throw new ArgumentNullException(nameof(localizer));
MessageQueue = messageQueue ?? throw new ArgumentNullException(nameof(messageQueue));
}
public Task Handle(Update update, CancellationToken cancellationToken = default)
{
long telegramId;
switch (update.Type)
{
case UpdateType.Message:
telegramId = update.Message.From.Id;
break;
case UpdateType.CallbackQuery:
telegramId = update.CallbackQuery.From.Id;
break;
default:
throw new ArgumentOutOfRangeException(nameof(update.Type), "Unsupported update type");
}
var message = new TextMessage(telegramId)
{
Text = Localizer.Get(nameof(StartMessageHandler), "Text"),
ParseMode = ParseMode.Html,
ReplyMarkup = GetKeyboard()
};
if (update.Type == UpdateType.CallbackQuery)
{
return Bot.EditMessageTextAsync(update.CallbackQuery.Message.MessageId, message, cancellationToken);
}
MessageQueue.Enqueue(message);
return Task.CompletedTask;
}
private IReplyMarkup GetKeyboard()
{
var markup = new VerticalKeyboardMarkup();
markup.Add(new InlineKeyboardButton(Localizer.Get(nameof(StartMessageHandler), "SubscriptionsButton"))
{
CallbackData = NocrCallbackData.ViewSubscription().ToString()
});
return markup.InlineKeyboardMarkup;
}
}

View File

@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging;
using Nocr.TelegramClient.AppServices.Bots;
using Nocr.TelegramClient.AppServices.Bots.MessageDispatcher;
using Nocr.TelegramClient.AppServices.TextSubscriptions;
using Nocr.TelegramClient.AppServices.Users;
using Nocr.TextMatcher.Api.Contracts.TextMatches;
using Nocr.TextMatcher.Api.Contracts.TextMatches.Dto;
using Telegram.Bot.Types.Enums;
@ -16,6 +17,7 @@ namespace Nocr.TelegramClient.AppServices.Handlers.BaseHandlers;
public abstract class ViewSubscriptionHandlerBase
{
protected IUsersService UsersService { get; }
protected ILogger Logger { get; }
protected IBot Bot { get; }
protected ITextSubscriptionsController SubscriptionsController { get; }
@ -23,8 +25,9 @@ public abstract class ViewSubscriptionHandlerBase
protected IMessageDispatcherQueue MessageQueue { get; }
protected ViewSubscriptionHandlerBase(ILogger logger, ILocalizer localizer, IMessageDispatcherQueue messageQueue,
IBot bot, ITextSubscriptionsController subscriptionsController)
IBot bot, IUsersService usersService, ITextSubscriptionsController subscriptionsController)
{
UsersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
SubscriptionsController =
@ -33,26 +36,53 @@ public abstract class ViewSubscriptionHandlerBase
MessageQueue = messageQueue ?? throw new ArgumentNullException(nameof(messageQueue));
}
public async Task SendSubscriptionMessage(long receiverId, TextSubscriptionData subscription,
public async Task SendSubscriptionMessage(long receiverId, long? subscriptionId,
CancellationToken cancellationToken = default)
{
var message = await GetMessage(receiverId, subscription, cancellationToken);
var message = await GetMessage(receiverId, subscriptionId, cancellationToken);
MessageQueue.Enqueue(message);
}
public async Task EditSubscriptionMessage(long receiverId, int messageId, long subscriptionId,
public async Task EditSubscriptionMessage(long receiverId, int messageId, long? subscriptionId,
CancellationToken cancellationToken = default)
{
var subscription = await SubscriptionsController.GetById(subscriptionId, cancellationToken);
var message = await GetMessage(receiverId, subscription, cancellationToken);
var message = await GetMessage(receiverId, subscriptionId, cancellationToken);
await Bot.EditOrSendTextMessage(messageId, message, Logger, cancellationToken);
}
private async Task<TextMessage> GetMessage(long receiverId, TextSubscriptionData subscription,
private async Task<TextMessage> GetMessage(long receiverId, long? subscriptionId,
CancellationToken cancellationToken = default)
{
var subscriptions = await SubscriptionsController.GetByUserId(subscription.UserId, cancellationToken);
// note(nazarovsa): Works only if receiverId is if of an user. If channel - refactor arguments
var user = await UsersService.GetByIdentity(receiverId, cancellationToken);
if (user == null)
{
throw new NotImplementedException($"User with telegramId {receiverId} not found");
}
var subscriptions = await SubscriptionsController.GetByUserId(user.Id, cancellationToken);
if (!subscriptions.Any())
{
throw new NotImplementedException($"There is no subscriptions for user with id {user.Id}");
}
var ordered = subscriptions.OrderBy(x => x.Id).ToList();
TextSubscriptionData? subscription = null;
if (subscriptionId.HasValue)
{
subscription = subscriptions.FirstOrDefault(x => x.Id == subscriptionId.Value);
if (subscription == null)
{
throw new NotImplementedException($"Subscription with id {subscriptionId.Value} not found");
}
}
else
{
subscription = ordered.First();
}
var indexOf = ordered.FindIndex(x => x.Id == subscription.Id);
long? prevId = indexOf > 0 ? ordered[indexOf - 1].Id : null;
long? nextId = indexOf < ordered.Count - 1 ? ordered[indexOf + 1].Id : null;
@ -95,7 +125,7 @@ public abstract class ViewSubscriptionHandlerBase
var deleteButtonText = Localizer.Get("Buttons", "Delete");
var markup = new VerticalKeyboardMarkup();
var row = new List<InlineKeyboardButton>(2);
if (prevId.HasValue)
{
@ -122,10 +152,14 @@ public abstract class ViewSubscriptionHandlerBase
? NocrCallbackData.DeactivateSubscription(textSubscription.Id).ToString()
: NocrCallbackData.ActivateSubscription(textSubscription.Id).ToString()
});
markup.Add(new InlineKeyboardButton(deleteButtonText)
{ CallbackData = NocrCallbackData.DeleteSubscription(textSubscription.Id).ToString() });
markup.Add(new InlineKeyboardButton(Localizer.Get("Buttons", "Back"))
{
CallbackData = new NocrCallbackData(NocrState.Start).ToString()
});
return markup;
}

View File

@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Nocr.TelegramClient.AppServices.Bots;
using Nocr.TelegramClient.AppServices.Bots.MessageDispatcher;
using Nocr.TelegramClient.AppServices.Handlers.BaseHandlers;
using Nocr.TelegramClient.AppServices.Users;
using Nocr.TextMatcher.Api.Contracts.TextMatches;
using Telegram.Bot.Types;
@ -17,8 +18,9 @@ public class ActivateSubscriptionHandler : ViewSubscriptionHandlerBase, IMatchin
ILocalizer localizer,
IMessageDispatcherQueue messageQueue,
IBot bot,
IUsersService usersService,
ITextSubscriptionsController subscriptionsController)
: base(logger, localizer, messageQueue, bot, subscriptionsController)
: base(logger, localizer, messageQueue, bot, usersService, subscriptionsController)
{
}

View File

@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Nocr.TelegramClient.AppServices.Bots;
using Nocr.TelegramClient.AppServices.Bots.MessageDispatcher;
using Nocr.TelegramClient.AppServices.Handlers.BaseHandlers;
using Nocr.TelegramClient.AppServices.Users;
using Nocr.TextMatcher.Api.Contracts.TextMatches;
using Telegram.Bot.Types;
@ -19,8 +20,9 @@ public class DeactivateSubscriptionHandler : ViewSubscriptionHandlerBase,
ILocalizer localizer,
IMessageDispatcherQueue messageQueue,
IBot bot,
IUsersService usersService,
ITextSubscriptionsController subscriptionsController)
: base(logger, localizer, messageQueue, bot, subscriptionsController)
: base(logger, localizer, messageQueue, bot, usersService, subscriptionsController)
{
}

View File

@ -8,8 +8,10 @@ using Nocr.TelegramClient.AppServices.Bots;
using Nocr.TelegramClient.AppServices.Bots.MessageDispatcher;
using Nocr.TelegramClient.AppServices.Handlers.BaseHandlers;
using Nocr.TelegramClient.AppServices.Handlers.CallbackQueries.DeactivateSubscription;
using Nocr.TelegramClient.AppServices.Users;
using Nocr.TextMatcher.Api.Contracts.TextMatches;
using Telegram.Bot.Types;
using Telegram.Bot.Types.ReplyMarkups;
namespace Nocr.TelegramClient.AppServices.Handlers.CallbackQueries.DeleteSubscription;
@ -20,8 +22,9 @@ public class DeleteSubscriptionHandler : ViewSubscriptionHandlerBase,
ILocalizer localizer,
IMessageDispatcherQueue messageQueue,
IBot bot,
ITextSubscriptionsController subscriptionsController)
: base(logger, localizer, messageQueue, bot, subscriptionsController)
IUsersService usersService,
ITextSubscriptionsController subscriptionsController)
: base(logger, localizer, messageQueue, bot, usersService, subscriptionsController)
{
}
@ -53,10 +56,16 @@ public class DeleteSubscriptionHandler : ViewSubscriptionHandlerBase,
var message = new TextMessage(from)
{
Text = Localizer.Get(nameof(DeleteSubscriptionHandler), "Text")
.FormatWith(new { Id = subscriptionId })
.FormatWith(new { Id = subscriptionId }),
ReplyMarkup = new InlineKeyboardMarkup(
new InlineKeyboardButton(Localizer.Get("Buttons", "Back"))
{
CallbackData = NocrCallbackData.ViewSubscription().ToString()
})
};
await Bot.EditOrSendTextMessage(update.CallbackQuery.Message.MessageId, message, Logger, CancellationToken.None);
await Bot.EditOrSendTextMessage(update.CallbackQuery.Message.MessageId, message, Logger,
CancellationToken.None);
}
private void SendErrorMessage(long from)

View File

@ -0,0 +1,15 @@
using Insight.Localizer;
using Insight.TelegramBot;
using Insight.TelegramBot.Handling.Handlers;
using Nocr.TelegramClient.AppServices.Bots.MessageDispatcher;
using Nocr.TelegramClient.AppServices.Handlers.BaseHandlers;
namespace Nocr.TelegramClient.AppServices.Handlers.CallbackQueries.Start;
public class StartHandler : StartHandlerBase, IMatchingUpdateHandler<StartMatcher>
{
public StartHandler(ILocalizer localizer, IMessageDispatcherQueue messageQueue, IBot bot)
: base(localizer, messageQueue, bot)
{
}
}

View File

@ -0,0 +1,12 @@
using Insight.TelegramBot.Handling.Matchers.CallbackQueryMatchers;
using Nocr.TelegramClient.AppServices.Bots;
namespace Nocr.TelegramClient.AppServices.Handlers.CallbackQueries.Start;
public class StartMatcher : StateCallbackQueryMatcher<NocrState>
{
public StartMatcher()
{
ExpectingState = NocrState.Start;
}
}

View File

@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Nocr.TelegramClient.AppServices.Bots;
using Nocr.TelegramClient.AppServices.Bots.MessageDispatcher;
using Nocr.TelegramClient.AppServices.Handlers.BaseHandlers;
using Nocr.TelegramClient.AppServices.Users;
using Nocr.TextMatcher.Api.Contracts.TextMatches;
using Telegram.Bot.Types;
@ -17,8 +18,9 @@ public class ViewSubscriptionHandler : ViewSubscriptionHandlerBase, IMatchingUpd
ILocalizer localizer,
IMessageDispatcherQueue messageQueue,
IBot bot,
IUsersService usersService,
ITextSubscriptionsController subscriptionsController) :
base(logger, localizer, messageQueue, bot, subscriptionsController)
base(logger, localizer, messageQueue, bot, usersService, subscriptionsController)
{
}
@ -27,13 +29,10 @@ public class ViewSubscriptionHandler : ViewSubscriptionHandlerBase, IMatchingUpd
var callbackData = NocrCallbackData.Parse(update.CallbackQuery.Data);
var from = update.CallbackQuery.From.Id;
if (callbackData.Args.Count != 1 || !long.TryParse(callbackData.Args.First(), out var subscriptionId))
long? subscriptionId = null;
if (callbackData.Args.Count != 0 && long.TryParse(callbackData.Args.First(), out var subscriptionIdFromMessage))
{
Logger.LogWarning(
"Не удалось извлечь идентификатор подписки. CallbackData: {@CallbackData}, ChatId: {ChatId}, MessageId {MessageId}",
callbackData, from, update.CallbackQuery.Message.Chat.Id);
SendErrorMessage(from);
return;
subscriptionId = subscriptionIdFromMessage;
}
await EditSubscriptionMessage(from, update.CallbackQuery.Message.MessageId, subscriptionId, cancellationToken);

View File

@ -1,33 +1,16 @@
using Insight.Localizer;
using Insight.TelegramBot;
using Insight.TelegramBot.Handling.Handlers;
using Insight.TelegramBot.Models;
using Nocr.TelegramClient.AppServices.Bots.MessageDispatcher;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Nocr.TelegramClient.AppServices.Handlers.BaseHandlers;
namespace Nocr.TelegramClient.AppServices.Handlers.Messages.StartMessage;
public class StartMessageHandler : IMatchingUpdateHandler<StartMessageMatcher>
public class StartMessageHandler : StartHandlerBase, IMatchingUpdateHandler<StartMessageMatcher>
{
private readonly ILocalizer _localizer;
private readonly IMessageDispatcherQueue _messageQueue;
public StartMessageHandler(ILocalizer localizer, IMessageDispatcherQueue messageQueue)
public StartMessageHandler(ILocalizer localizer, IMessageDispatcherQueue messageQueue, IBot bot)
:base(localizer, messageQueue, bot)
{
_localizer = localizer ?? throw new ArgumentNullException(nameof(localizer));
_messageQueue = messageQueue ?? throw new ArgumentNullException(nameof(messageQueue));
}
public Task Handle(Update update, CancellationToken cancellationToken = default)
{
var telegramId = update.Message.From.Id;
var message = new TextMessage(telegramId)
{
Text = _localizer.Get(nameof(StartMessageHandler), "Text"),
ParseMode = ParseMode.Html
};
_messageQueue.Enqueue(message);
return Task.CompletedTask;
}
}

View File

@ -17,8 +17,6 @@ namespace Nocr.TelegramClient.AppServices.Handlers.Messages.SubscribeMessage;
public class SubscribeMessageHandler : ViewSubscriptionHandlerBase, IMatchingUpdateHandler<SubscribeMessageMatcher>
{
private readonly IUsersService _usersService;
/// <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 "обувь ботинки сапоги".
@ -34,9 +32,8 @@ public class SubscribeMessageHandler : ViewSubscriptionHandlerBase, IMatchingUpd
IUsersService usersService,
IBot bot,
ITextSubscriptionsController subscriptionsController)
: base(logger, localizer, messageQueue, bot, subscriptionsController)
: base(logger, localizer, messageQueue, bot, usersService, subscriptionsController)
{
_usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
}
public async Task Handle(Update update, CancellationToken cancellationToken = default)
@ -65,7 +62,7 @@ public class SubscribeMessageHandler : ViewSubscriptionHandlerBase, IMatchingUpd
var template = match.Groups[3].Value;
var user = await _usersService.GetOrCreate(receiverId, update.Message.From.Username, cancellationToken);
var user = await UsersService.GetOrCreate(receiverId, update.Message.From.Username, cancellationToken);
var subscriptionId = await SubscriptionsController.Create(new CreateTextSubscriptionRequest
{
UserId = user.Id,
@ -80,7 +77,6 @@ public class SubscribeMessageHandler : ViewSubscriptionHandlerBase, IMatchingUpd
.FormatWith(new { Id = subscriptionId })
});
var subscription = await SubscriptionsController.GetById(subscriptionId, CancellationToken.None);
await SendSubscriptionMessage(receiverId, subscription, CancellationToken.None);
await SendSubscriptionMessage(receiverId, subscriptionId, CancellationToken.None);
}
}

View File

@ -1,40 +0,0 @@
using Insight.Localizer;
using Insight.TelegramBot;
using Insight.TelegramBot.Handling.Handlers;
using Microsoft.Extensions.Logging;
using Nocr.TelegramClient.AppServices.Bots.MessageDispatcher;
using Nocr.TelegramClient.AppServices.Handlers.BaseHandlers;
using Nocr.TelegramClient.AppServices.Users;
using Nocr.TextMatcher.Api.Contracts.TextMatches;
using Telegram.Bot.Types;
namespace Nocr.TelegramClient.AppServices.Handlers.Messages.SubscriptionsMessage;
public class SubscriptionsMessageHandler : ViewSubscriptionHandlerBase,
IMatchingUpdateHandler<SubscriptionsMessageMatcher>
{
private readonly IUsersService _usersService;
public SubscriptionsMessageHandler(
ILogger<SubscriptionsMessageHandler> logger,
ILocalizer localizer,
IMessageDispatcherQueue messageQueue,
IUsersService usersService,
IBot bot,
ITextSubscriptionsController subscriptionsController)
: base(logger, localizer, messageQueue, bot, subscriptionsController)
{
_usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
}
public async Task Handle(Update update, CancellationToken cancellationToken = default)
{
var telegramId = update.Message.From.Id;
var user = await _usersService.GetOrCreate(telegramId, update.Message.From.Username, cancellationToken);
var subscriptions = await SubscriptionsController.GetByUserId(user.Id, cancellationToken);
var subscription = subscriptions.MinBy(x => x.Id);
await SendSubscriptionMessage(telegramId, subscription, CancellationToken.None);
}
}

View File

@ -1,11 +0,0 @@
using Insight.TelegramBot.Handling.Matchers.TextMatchers;
namespace Nocr.TelegramClient.AppServices.Handlers.Messages.SubscriptionsMessage;
public class SubscriptionsMessageMatcher : TextStartWithUpdateMatcher
{
public SubscriptionsMessageMatcher()
{
Template = "/subscriptions";
}
}

View File

@ -5,6 +5,8 @@ namespace Nocr.TelegramClient.AppServices.Users;
public interface IUsersService
{
public Task<UserData> GetOrCreate(long telegramId, string? username, CancellationToken cancellationToken = default);
public Task<UserData?> GetByIdentity(long telegramId, CancellationToken cancellationToken = default);
public Task<UserData?> GetById(long id, CancellationToken cancellationToken = default);
}

View File

@ -13,9 +13,11 @@ public sealed class UsersService : IUsersService
_usersController = usersController ?? throw new ArgumentNullException(nameof(usersController));
}
public async Task<UserData> GetOrCreate(long telegramId, string? username, CancellationToken cancellationToken = default)
public async Task<UserData> GetOrCreate(long telegramId, string? username,
CancellationToken cancellationToken = default)
{
var user = await _usersController.GetByIdentity(UserIdentityType.TelegramId, telegramId.ToString(), cancellationToken);
var user = await _usersController.GetByIdentity(UserIdentityType.TelegramId, telegramId.ToString(),
cancellationToken);
if (user == null)
{
await _usersController.Create(new CreateUserRequest
@ -32,6 +34,11 @@ public sealed class UsersService : IUsersService
return user;
}
public Task<UserData?> GetByIdentity(long telegramId, CancellationToken cancellationToken = default)
{
return _usersController.GetByIdentity(UserIdentityType.TelegramId, telegramId.ToString(), cancellationToken);
}
public Task<UserData?> GetById(long id, CancellationToken cancellationToken = default)
{
return _usersController.GetById(id, cancellationToken);

View File

@ -25,5 +25,9 @@
<Content Include="Resources" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Resources\Handlers\TelegramHandlers\Messages\StartMessage\StartMessageHandler.ru-ru.json" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,5 @@
{
"Text": "Привет! Я, Nocr 🤖!",
"SubscriptionsButton": "Подписки 📩"
}