From 67a3457f2d8ed4f7081cda81a27ee6d558587bfa Mon Sep 17 00:00:00 2001 From: Lisoveliy Date: Sat, 12 Jul 2025 01:24:39 +0300 Subject: [PATCH 1/3] WIP: implemented registration and EULA accept, WIP on auth issue --- JOBot.TClient/ButtonResource.Designer.cs | 71 +++++++++++++++ JOBot.TClient/ButtonResource.resx | 24 +++++ .../Buttons/EulaAgreementButtonCommand.cs | 17 ++++ .../Commands/IAuthorizedTelegramCommand.cs | 15 ++++ JOBot.TClient/Commands/StartCommand.cs | 18 ++-- .../HostedServices/BotBackgroundService.cs | 41 +++++++-- JOBot.TClient/Core/Services/UserService.cs | 18 ---- JOBot.TClient/DependencyInjection.cs | 27 ++++-- .../Exceptions/FallbackException.cs | 17 ++++ JOBot.TClient/JOBot.TClient.csproj | 83 ++++++++++------- JOBot.TClient/Program.cs | 13 +-- JOBot.TClient/Services/MenuService.cs | 17 ++++ JOBot.TClient/Services/PrepareUserService.cs | 89 ++++++++++++++++++ JOBot.TClient/Services/UserService.cs | 41 +++++++++ JOBot.TClient/Statements/PrepareUserState.cs | 44 +++++++++ JOBot.TClient/TextResource.Designer.cs | 90 +++++++++++++++++++ JOBot.TClient/TextResource.resx | 31 +++++++ 17 files changed, 569 insertions(+), 87 deletions(-) create mode 100644 JOBot.TClient/ButtonResource.Designer.cs create mode 100644 JOBot.TClient/ButtonResource.resx create mode 100644 JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs create mode 100644 JOBot.TClient/Commands/IAuthorizedTelegramCommand.cs delete mode 100644 JOBot.TClient/Core/Services/UserService.cs create mode 100644 JOBot.TClient/Infrastructure/Exceptions/FallbackException.cs create mode 100644 JOBot.TClient/Services/MenuService.cs create mode 100644 JOBot.TClient/Services/PrepareUserService.cs create mode 100644 JOBot.TClient/Services/UserService.cs create mode 100644 JOBot.TClient/Statements/PrepareUserState.cs create mode 100644 JOBot.TClient/TextResource.Designer.cs create mode 100644 JOBot.TClient/TextResource.resx diff --git a/JOBot.TClient/ButtonResource.Designer.cs b/JOBot.TClient/ButtonResource.Designer.cs new file mode 100644 index 0000000..aba5cf0 --- /dev/null +++ b/JOBot.TClient/ButtonResource.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace JOBot.TClient { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class ButtonResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ButtonResource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("JOBot.TClient.ButtonResource", typeof(ButtonResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Соглашаюсь с условиями использования ✅. + /// + public static string EULAAgrement { + get { + return ResourceManager.GetString("EULAAgrement", resourceCulture); + } + } + } +} diff --git a/JOBot.TClient/ButtonResource.resx b/JOBot.TClient/ButtonResource.resx new file mode 100644 index 0000000..a799cfc --- /dev/null +++ b/JOBot.TClient/ButtonResource.resx @@ -0,0 +1,24 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Соглашаюсь с условиями использования ✅ + + \ No newline at end of file diff --git a/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs b/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs new file mode 100644 index 0000000..3ff584b --- /dev/null +++ b/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs @@ -0,0 +1,17 @@ +using JOBot.Proto; +using JOBot.TClient.Services; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace JOBot.TClient.Commands.Buttons; + +public class EulaAgreementButtonCommand(PrepareUserService prepareUserService) : IAuthorizedTelegramCommand +{ + public async Task ExecuteAsync(Update update, GetUserResponse user, CancellationToken ct) + { + if (update.Type != UpdateType.Message || update.Message?.From == null) + return; + + await prepareUserService.AcceptEula(update, ct); + } +} \ No newline at end of file diff --git a/JOBot.TClient/Commands/IAuthorizedTelegramCommand.cs b/JOBot.TClient/Commands/IAuthorizedTelegramCommand.cs new file mode 100644 index 0000000..4d37881 --- /dev/null +++ b/JOBot.TClient/Commands/IAuthorizedTelegramCommand.cs @@ -0,0 +1,15 @@ +using JOBot.Proto; +using Telegram.Bot; +using Telegram.Bot.Types; + +namespace JOBot.TClient.Commands; + +public interface IAuthorizedTelegramCommand : ITelegramCommand +{ + public Task ExecuteAsync(Update update, GetUserResponse user, CancellationToken ct); + + Task ITelegramCommand.ExecuteAsync(Update update, CancellationToken ct) + { + throw new UnauthorizedAccessException("You do not have permission to access this command."); + } +} \ No newline at end of file diff --git a/JOBot.TClient/Commands/StartCommand.cs b/JOBot.TClient/Commands/StartCommand.cs index 9d59b07..b2f5bb4 100644 --- a/JOBot.TClient/Commands/StartCommand.cs +++ b/JOBot.TClient/Commands/StartCommand.cs @@ -1,22 +1,14 @@ -using JOBot.TClient.Core.Services; -using Telegram.Bot; +using JOBot.TClient.Commands.Buttons; +using JOBot.TClient.Statements; using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; namespace JOBot.TClient.Commands; -public class StartCommand(ITelegramBotClient bot, UserService userService) : ITelegramCommand +public class StartCommand(PrepareUserState prepareUserState) : ITelegramCommand { - private const string ReturnMessage = "Привет! Я JOBot, помощник по поиску работы в IT."; public async Task ExecuteAsync(Update update, CancellationToken ct) { - await userService.RegisterAsync( - update.Message!.Chat.Id, - update.Message.Chat.Username); - - await bot.SendMessage(chatId: update.Message.Chat.Id, - "Продолжая, вы принимаете политику конфиденциальности и правила сервиса" + - "\nhttps://hh.ru/article/personal_data?backurl=%2F&role=applicant" + - "\nhttps://hh.ru/account/agreement?backurl=%2Faccount%2Fsignup%3Fbackurl%3D%252F%26role%3Dapplicant&role=applicant", - cancellationToken: ct); + await prepareUserState.TryToPrepareUser(update, ct); } } \ No newline at end of file diff --git a/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs b/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs index c342862..426326f 100644 --- a/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs +++ b/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs @@ -1,17 +1,19 @@ using JOBot.TClient.Commands; +using JOBot.TClient.Commands.Buttons; +using JOBot.TClient.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; - -namespace JOBot.TClient.Core.HostedServices; - using Telegram.Bot; using Telegram.Bot.Types; using Microsoft.Extensions.Hosting; +namespace JOBot.TClient.Core.HostedServices; + public sealed class BotBackgroundService( ITelegramBotClient botClient, IServiceProvider services, - ILogger logger) + ILogger logger, + UserService userService) : BackgroundService { protected override Task ExecuteAsync(CancellationToken stoppingToken) @@ -20,7 +22,7 @@ public sealed class BotBackgroundService( updateHandler: HandleUpdateAsync, errorHandler: HandleErrorAsync, cancellationToken: stoppingToken); - + return Task.CompletedTask; } @@ -29,11 +31,34 @@ public sealed class BotBackgroundService( using var scope = services.CreateScope(); var commands = new Dictionary { - ["/start"] = scope.ServiceProvider.GetRequiredService() + //Commands + ["/start"] = scope.ServiceProvider.GetRequiredService(), + ["/menu"] = scope.ServiceProvider.GetRequiredService(), + + //Buttons + [ButtonResource.EULAAgrement] = scope.ServiceProvider.GetRequiredService(), }; - if (update.Message?.Text is { } text && commands.TryGetValue(text, out var command)) - await command.ExecuteAsync(update, ct); + if (update.Message?.Text is { } text && update.Message?.From != null) + { + var user = await userService.GetUser(update, ct); //Check user for existance + + if (user == null) + { + await commands["/start"].ExecuteAsync(update, ct); + return; + } + + if (commands.TryGetValue(text, out var command)) + { + if (command is IAuthorizedTelegramCommand authorizedTelegramCommand) + await authorizedTelegramCommand.ExecuteAsync(update, user, ct); + else await command.ExecuteAsync(update, ct); //А если вызвать /start когда зареган? То-то и оно, но всё равно надо подумать + return; + } + + await bot.SendMessage(update.Message.From.Id, TextResource.CommandNotFound, cancellationToken: ct); + } } private Task HandleErrorAsync(ITelegramBotClient bot, Exception ex, CancellationToken ct) diff --git a/JOBot.TClient/Core/Services/UserService.cs b/JOBot.TClient/Core/Services/UserService.cs deleted file mode 100644 index c59bcb8..0000000 --- a/JOBot.TClient/Core/Services/UserService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using User = JOBot.Proto.User; - -namespace JOBot.TClient.Core.Services; - -public class UserService(User.UserClient client) -{ - public async Task RegisterAsync(long userId, string? username) - { - - var response = await client.RegisterAsync(new() - { - UserId = userId, - Username = username - }); - - return response.Success; - } -} \ No newline at end of file diff --git a/JOBot.TClient/DependencyInjection.cs b/JOBot.TClient/DependencyInjection.cs index 5341d5f..faec336 100644 --- a/JOBot.TClient/DependencyInjection.cs +++ b/JOBot.TClient/DependencyInjection.cs @@ -2,7 +2,10 @@ using Grpc.Core; using Grpc.Net.Client; using JOBot.Proto; using JOBot.TClient.Commands; -using JOBot.TClient.Core.Services; +using JOBot.TClient.Commands.Buttons; +using JOBot.TClient.Core.HostedServices; +using JOBot.TClient.Services; +using JOBot.TClient.Statements; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -19,15 +22,29 @@ public static class DependencyInjection services.AddScoped(_ => GrpcChannel.ForAddress(config.GetValue("BackendHost") ?? throw new MissingFieldException("Host is not defined"))); - //Commands + #region Commands services.AddScoped(); + //buttons + services.AddScoped(); + #endregion + + #region gRPC Clients + services.AddGrpcClient(o => o.Address = new Uri("http://backend:5001")); + #endregion + + #region Services services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + #endregion - //gRPC Clients - services.AddScoped(); + #region States + services.AddScoped(); + #endregion - // Telegram Bot + // Bot service + services.AddHostedService(); services.AddSingleton(_ => new TelegramBotClient(config.GetValue("TelegramToken") ?? throw new MissingFieldException("TelegramToken is not set"))); diff --git a/JOBot.TClient/Infrastructure/Exceptions/FallbackException.cs b/JOBot.TClient/Infrastructure/Exceptions/FallbackException.cs new file mode 100644 index 0000000..e624813 --- /dev/null +++ b/JOBot.TClient/Infrastructure/Exceptions/FallbackException.cs @@ -0,0 +1,17 @@ +using Telegram.Bot; +using Telegram.Bot.Types; + +namespace JOBot.TClient.Infrastructure.Exceptions; + +/// +/// Exception for fallback +/// WARNING: Don't create new exception without throwing it +/// +public class FallbackException : Exception +{ + public FallbackException(string message, Message botMessage, ITelegramBotClient botClient) : base(message) + { + botClient.SendMessage(chatId: botMessage.Chat.Id, + TextResource.FallbackMessage); + } +} \ No newline at end of file diff --git a/JOBot.TClient/JOBot.TClient.csproj b/JOBot.TClient/JOBot.TClient.csproj index 1639923..e8b6287 100644 --- a/JOBot.TClient/JOBot.TClient.csproj +++ b/JOBot.TClient/JOBot.TClient.csproj @@ -1,39 +1,60 @@  - - Exe - net9.0 - enable - enable - + + Exe + net9.0 + enable + enable + - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + - - - + + + - - - Always - - + + + Always + + - - - + + + True + True + ButtonResource.resx + + + True + True + TextResource.resx + + + + + + PublicResXFileCodeGenerator + TextResource.Designer.cs + + + PublicResXFileCodeGenerator + ButtonResource.Designer.cs + + diff --git a/JOBot.TClient/Program.cs b/JOBot.TClient/Program.cs index a352217..5c14977 100644 --- a/JOBot.TClient/Program.cs +++ b/JOBot.TClient/Program.cs @@ -1,9 +1,4 @@ -using JOBot.Proto; -using JOBot.TClient; -using JOBot.TClient.Commands; -using JOBot.TClient.Core.HostedServices; -using JOBot.TClient.Core.Services; -using Microsoft.Extensions.DependencyInjection; +using JOBot.TClient; using Microsoft.Extensions.Hosting; var host = Host.CreateDefaultBuilder(args) @@ -12,12 +7,6 @@ var host = Host.CreateDefaultBuilder(args) // Настройка DI services.ConfigureServices(context.Configuration); - // Фоновый сервис для бота - services.AddHostedService(); - - services.AddScoped(); - services.AddSingleton(); - services.AddGrpcClient(o => o.Address = new Uri("http://backend:5001")); }) .Build(); diff --git a/JOBot.TClient/Services/MenuService.cs b/JOBot.TClient/Services/MenuService.cs new file mode 100644 index 0000000..9849490 --- /dev/null +++ b/JOBot.TClient/Services/MenuService.cs @@ -0,0 +1,17 @@ +using Telegram.Bot; +using Telegram.Bot.Types; +using User = JOBot.Proto.User; + +namespace JOBot.TClient.Services; + +public class MenuService(ITelegramBotClient bot) +{ + public Task RenderMenu(Update update, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(update.Message?.From); + + bot.SendMessage(update.Message.From.Id,"PrepareUser stage is done.", cancellationToken: ct); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/JOBot.TClient/Services/PrepareUserService.cs b/JOBot.TClient/Services/PrepareUserService.cs new file mode 100644 index 0000000..53c753d --- /dev/null +++ b/JOBot.TClient/Services/PrepareUserService.cs @@ -0,0 +1,89 @@ +using Grpc.Core; +using JetBrains.Annotations; +using JOBot.Proto; +using JOBot.TClient.Infrastructure.Exceptions; +using Telegram.Bot; +using Telegram.Bot.Types; +using User = JOBot.Proto.User; + +namespace JOBot.TClient.Services; + +[UsedImplicitly] +public class PrepareUserService(ITelegramBotClient bot, User.UserClient userClient) : UserService(bot, userClient) +{ + private readonly ITelegramBotClient _bot = bot; + private readonly User.UserClient _userClient = userClient; + + /// + /// Get or register user on system + /// + /// Telegram Update object + /// Cancellation Token + /// RPC User Response + /// If something in server logic went wrong + /// If update.Message is null + public async Task RegisterUser(Update update, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(update.Message?.From); + + var result = await _userClient.RegisterAsync(new RegisterRequest + { + UserId = update.Message.From.Id, + Username = update.Message.From.Username, + }); + + if (!result.Success) + { + await _bot.SendMessage(chatId: update.Message.Chat.Id, + TextResource.FallbackMessage, + cancellationToken: ct); + + throw new FallbackException(TextResource.FallbackMessage, update.Message, _bot); + } + + var user = await _userClient.GetUserAsync(new GetUserRequest() + { + UserId = update.Message.From.Id, + }); + + if (user == null) + throw new FallbackException(TextResource.FallbackMessage, update.Message, _bot); + + return user; + } + + /// + /// Get Eula Agreement from user + /// + /// Telegram Update object + /// Cancellation Token + /// If update.Message is null + public async Task AskForEulaAgreement(Update update, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(update.Message?.From); + + await _bot.SendMessage( + update.Message.From.Id, + TextResource.EULA, + replyMarkup: new[] { ButtonResource.EULAAgrement }, cancellationToken: ct); + } + + /// + /// Accept EULA for user + /// + /// Telegram update object + /// Cancellation Token + /// If something in server logic went wrong + public async Task AcceptEula(Update update, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(update.Message?.From); + var result = await _userClient.AcceptEulaAsync(new() + { + EulaAccepted = true, + UserId = update.Message.From.Id, + }); + + if (!result.Success) + throw new FallbackException(TextResource.FallbackMessage, update.Message, _bot); + } +} \ No newline at end of file diff --git a/JOBot.TClient/Services/UserService.cs b/JOBot.TClient/Services/UserService.cs new file mode 100644 index 0000000..51f4c7b --- /dev/null +++ b/JOBot.TClient/Services/UserService.cs @@ -0,0 +1,41 @@ +using Grpc.Core; +using JOBot.Proto; +using JOBot.TClient.Infrastructure.Exceptions; +using Telegram.Bot; +using Telegram.Bot.Types; +using User = JOBot.Proto.User; + +namespace JOBot.TClient.Services; + +public class UserService(ITelegramBotClient bot, User.UserClient userClient) +{ + /// + /// Get user + /// + /// Telegram Update object + /// Cancellation Token + /// RPC User Response or null if user not exists + /// If something in server logic went wrong + /// If update.Message is null + public async Task GetUser(Update update, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(update.Message?.From); + + GetUserResponse? user; + try + { + user = await userClient.GetUserAsync(new GetUserRequest() //Получаем пользователя + { + UserId = update.Message.From.Id, + }); + } + catch (RpcException e) //Пользователь не найден? + { + if (e.StatusCode != StatusCode.NotFound) + throw new FallbackException(TextResource.FallbackMessage, update.Message, bot); + + return null; + } + return user; + } +} \ No newline at end of file diff --git a/JOBot.TClient/Statements/PrepareUserState.cs b/JOBot.TClient/Statements/PrepareUserState.cs new file mode 100644 index 0000000..11412a6 --- /dev/null +++ b/JOBot.TClient/Statements/PrepareUserState.cs @@ -0,0 +1,44 @@ +using JOBot.TClient.Services; +using Telegram.Bot.Types; +using User = JOBot.Proto.User; + +namespace JOBot.TClient.Statements; + +public class PrepareUserState(PrepareUserService prepareUserService, MenuService menuService) +{ + /// + /// Try to prepare user if is not registered + /// + /// Update telegram object + /// Cancellation token + public async Task TryToPrepareUser(Update update, CancellationToken ct = default) + { + var user = await prepareUserService.GetUser(update, ct); + if (user == null) + user = await prepareUserService.RegisterUser(update, ct); + + if (!user.Eula) + { + await prepareUserService.AskForEulaAgreement(update, ct); + return; //interrupt while eula isn't accepted + } + + await OnUserEulaValidStage(update, ct); + } + + /// + /// Signal for accepted eula + /// + /// + /// + public async Task UserAcceptedEula(Update update, CancellationToken ct = default) + { + await prepareUserService.AcceptEula(update, ct: ct); + await OnUserEulaValidStage(update, ct); + } + + private async Task OnUserEulaValidStage(Update update, CancellationToken ct = default) + { + await menuService.RenderMenu(update, ct); //boilerplate + } +} \ No newline at end of file diff --git a/JOBot.TClient/TextResource.Designer.cs b/JOBot.TClient/TextResource.Designer.cs new file mode 100644 index 0000000..05a2690 --- /dev/null +++ b/JOBot.TClient/TextResource.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace JOBot.TClient { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class TextResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TextResource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("JOBot.TClient.TextResource", typeof(TextResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Команда не найдена, попробуйте что-то другое. + /// + public static string CommandNotFound { + get { + return ResourceManager.GetString("CommandNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Продолжая, вы принимаете политику конфиденциальности и правила сервиса \nhttps://hh.ru/article/personal_data?backurl=%2F&role=applicant + ///\nhttps://hh.ru/account/agreement?backurl=%2Faccount%2Fsignup%3Fbackurl%3D%252F%26role%3Dapplicant&role=applicant". + /// + public static string EULA { + get { + return ResourceManager.GetString("EULA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Something went REALLY wrong. Service can't continue process.. + /// + public static string FallbackMessage { + get { + return ResourceManager.GetString("FallbackMessage", resourceCulture); + } + } + } +} diff --git a/JOBot.TClient/TextResource.resx b/JOBot.TClient/TextResource.resx new file mode 100644 index 0000000..75fc37f --- /dev/null +++ b/JOBot.TClient/TextResource.resx @@ -0,0 +1,31 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Продолжая, вы принимаете политику конфиденциальности и правила сервиса \nhttps://hh.ru/article/personal_data?backurl=%2F&role=applicant +\nhttps://hh.ru/account/agreement?backurl=%2Faccount%2Fsignup%3Fbackurl%3D%252F%26role%3Dapplicant&role=applicant" + + + Something went REALLY wrong. Service can't continue process. + + + Команда не найдена, попробуйте что-то другое + + \ No newline at end of file -- 2.47.1 From a526dbf6c6607e2784c935fce68380f6440b86a0 Mon Sep 17 00:00:00 2001 From: Lisoveliy Date: Sat, 12 Jul 2025 01:52:16 +0300 Subject: [PATCH 2/3] feat: implemented registration stage of preparation on PrepareUserState implemented auth, added auth tools AcceptNotPreparedAttribute.cs, IAuthorizedTelegramCommand.cs --- .../Buttons/EulaAgreementButtonCommand.cs | 7 +++++-- JOBot.TClient/Commands/InfoCommand.cs | 14 ++++++++++++++ JOBot.TClient/Commands/MenuCommand.cs | 13 +++++++++++++ .../Core/HostedServices/BotBackgroundService.cs | 15 +++++++++++++-- JOBot.TClient/DependencyInjection.cs | 2 ++ .../Authorization/AcceptNotPreparedAttribute.cs | 4 ++++ .../Extensions/GRpcModelsExtensions.cs | 9 +++++++++ JOBot.TClient/Services/PrepareUserService.cs | 3 --- JOBot.TClient/Statements/PrepareUserState.cs | 13 ++++++++----- JOBot.TClient/TextResource.Designer.cs | 9 +++++++++ JOBot.TClient/TextResource.resx | 3 +++ 11 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 JOBot.TClient/Commands/InfoCommand.cs create mode 100644 JOBot.TClient/Commands/MenuCommand.cs create mode 100644 JOBot.TClient/Infrastructure/Attributes/Authorization/AcceptNotPreparedAttribute.cs create mode 100644 JOBot.TClient/Infrastructure/Extensions/GRpcModelsExtensions.cs diff --git a/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs b/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs index 3ff584b..4f1c6d3 100644 --- a/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs +++ b/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs @@ -1,17 +1,20 @@ using JOBot.Proto; +using JOBot.TClient.Infrastructure.Attributes.Authorization; using JOBot.TClient.Services; +using JOBot.TClient.Statements; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; namespace JOBot.TClient.Commands.Buttons; -public class EulaAgreementButtonCommand(PrepareUserService prepareUserService) : IAuthorizedTelegramCommand +[AcceptNotPrepared] +public class EulaAgreementButtonCommand(PrepareUserState prepareUserState) : IAuthorizedTelegramCommand { public async Task ExecuteAsync(Update update, GetUserResponse user, CancellationToken ct) { if (update.Type != UpdateType.Message || update.Message?.From == null) return; - await prepareUserService.AcceptEula(update, ct); + await prepareUserState.AcceptEula(update, ct); } } \ No newline at end of file diff --git a/JOBot.TClient/Commands/InfoCommand.cs b/JOBot.TClient/Commands/InfoCommand.cs new file mode 100644 index 0000000..084fb2a --- /dev/null +++ b/JOBot.TClient/Commands/InfoCommand.cs @@ -0,0 +1,14 @@ +using Telegram.Bot; +using Telegram.Bot.Types; + +namespace JOBot.TClient.Commands; + +public class InfoCommand(ITelegramBotClient bot) : ITelegramCommand +{ + public async Task ExecuteAsync(Update update, CancellationToken ct) + { + ArgumentNullException.ThrowIfNull(update.Message?.From); + + await bot.SendMessage(update.Message.From.Id, TextResource.Info, cancellationToken: ct); + } +} \ No newline at end of file diff --git a/JOBot.TClient/Commands/MenuCommand.cs b/JOBot.TClient/Commands/MenuCommand.cs new file mode 100644 index 0000000..b5b7393 --- /dev/null +++ b/JOBot.TClient/Commands/MenuCommand.cs @@ -0,0 +1,13 @@ +using JOBot.Proto; +using JOBot.TClient.Services; +using Telegram.Bot.Types; + +namespace JOBot.TClient.Commands; + +public class MenuCommand(MenuService menuService) : IAuthorizedTelegramCommand +{ + public async Task ExecuteAsync(Update update, GetUserResponse user, CancellationToken ct) + { + await menuService.RenderMenu(update, ct); + } +} \ No newline at end of file diff --git a/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs b/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs index 426326f..82264e8 100644 --- a/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs +++ b/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs @@ -1,5 +1,7 @@ using JOBot.TClient.Commands; using JOBot.TClient.Commands.Buttons; +using JOBot.TClient.Infrastructure.Attributes.Authorization; +using JOBot.TClient.Infrastructure.Extensions; using JOBot.TClient.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -33,7 +35,8 @@ public sealed class BotBackgroundService( { //Commands ["/start"] = scope.ServiceProvider.GetRequiredService(), - ["/menu"] = scope.ServiceProvider.GetRequiredService(), + ["/menu"] = scope.ServiceProvider.GetRequiredService(), + ["/info"] = scope.ServiceProvider.GetRequiredService(), //Buttons [ButtonResource.EULAAgrement] = scope.ServiceProvider.GetRequiredService(), @@ -52,8 +55,16 @@ public sealed class BotBackgroundService( if (commands.TryGetValue(text, out var command)) { if (command is IAuthorizedTelegramCommand authorizedTelegramCommand) + { + var attribute = Attribute.GetCustomAttribute(command.GetType(), typeof(AcceptNotPreparedAttribute)); + if (!user.IsPrepared() && attribute is not AcceptNotPreparedAttribute) + { + await commands["/start"].ExecuteAsync(update, ct); //заставляем пользователя завершить регистрацию + return; + } await authorizedTelegramCommand.ExecuteAsync(update, user, ct); - else await command.ExecuteAsync(update, ct); //А если вызвать /start когда зареган? То-то и оно, но всё равно надо подумать + } + else await command.ExecuteAsync(update, ct); return; } diff --git a/JOBot.TClient/DependencyInjection.cs b/JOBot.TClient/DependencyInjection.cs index faec336..71a7e93 100644 --- a/JOBot.TClient/DependencyInjection.cs +++ b/JOBot.TClient/DependencyInjection.cs @@ -24,6 +24,8 @@ public static class DependencyInjection #region Commands services.AddScoped(); + services.AddScoped(); + services.AddScoped(); //buttons services.AddScoped(); diff --git a/JOBot.TClient/Infrastructure/Attributes/Authorization/AcceptNotPreparedAttribute.cs b/JOBot.TClient/Infrastructure/Attributes/Authorization/AcceptNotPreparedAttribute.cs new file mode 100644 index 0000000..7929497 --- /dev/null +++ b/JOBot.TClient/Infrastructure/Attributes/Authorization/AcceptNotPreparedAttribute.cs @@ -0,0 +1,4 @@ +namespace JOBot.TClient.Infrastructure.Attributes.Authorization; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public class AcceptNotPreparedAttribute : Attribute; \ No newline at end of file diff --git a/JOBot.TClient/Infrastructure/Extensions/GRpcModelsExtensions.cs b/JOBot.TClient/Infrastructure/Extensions/GRpcModelsExtensions.cs new file mode 100644 index 0000000..ba10820 --- /dev/null +++ b/JOBot.TClient/Infrastructure/Extensions/GRpcModelsExtensions.cs @@ -0,0 +1,9 @@ +using JOBot.Proto; + +namespace JOBot.TClient.Infrastructure.Extensions; + +public static class GRpcModelsExtensions +{ + public static bool IsPrepared(this GetUserResponse user) => user is { Eula: true, IsLogged: true } && + !string.IsNullOrEmpty(user.CVUrl); +} \ No newline at end of file diff --git a/JOBot.TClient/Services/PrepareUserService.cs b/JOBot.TClient/Services/PrepareUserService.cs index 53c753d..4f2921e 100644 --- a/JOBot.TClient/Services/PrepareUserService.cs +++ b/JOBot.TClient/Services/PrepareUserService.cs @@ -1,5 +1,3 @@ -using Grpc.Core; -using JetBrains.Annotations; using JOBot.Proto; using JOBot.TClient.Infrastructure.Exceptions; using Telegram.Bot; @@ -8,7 +6,6 @@ using User = JOBot.Proto.User; namespace JOBot.TClient.Services; -[UsedImplicitly] public class PrepareUserService(ITelegramBotClient bot, User.UserClient userClient) : UserService(bot, userClient) { private readonly ITelegramBotClient _bot = bot; diff --git a/JOBot.TClient/Statements/PrepareUserState.cs b/JOBot.TClient/Statements/PrepareUserState.cs index 11412a6..855ebe4 100644 --- a/JOBot.TClient/Statements/PrepareUserState.cs +++ b/JOBot.TClient/Statements/PrepareUserState.cs @@ -13,10 +13,8 @@ public class PrepareUserState(PrepareUserService prepareUserService, MenuService /// Cancellation token public async Task TryToPrepareUser(Update update, CancellationToken ct = default) { - var user = await prepareUserService.GetUser(update, ct); - if (user == null) - user = await prepareUserService.RegisterUser(update, ct); - + var user = await prepareUserService.GetUser(update, ct) ?? await prepareUserService.RegisterUser(update, ct); + if (!user.Eula) { await prepareUserService.AskForEulaAgreement(update, ct); @@ -31,12 +29,17 @@ public class PrepareUserState(PrepareUserService prepareUserService, MenuService /// /// /// - public async Task UserAcceptedEula(Update update, CancellationToken ct = default) + public async Task AcceptEula(Update update, CancellationToken ct = default) { await prepareUserService.AcceptEula(update, ct: ct); await OnUserEulaValidStage(update, ct); } + /// + /// Continue prepare stage + /// + /// + /// private async Task OnUserEulaValidStage(Update update, CancellationToken ct = default) { await menuService.RenderMenu(update, ct); //boilerplate diff --git a/JOBot.TClient/TextResource.Designer.cs b/JOBot.TClient/TextResource.Designer.cs index 05a2690..2e46a39 100644 --- a/JOBot.TClient/TextResource.Designer.cs +++ b/JOBot.TClient/TextResource.Designer.cs @@ -86,5 +86,14 @@ namespace JOBot.TClient { return ResourceManager.GetString("FallbackMessage", resourceCulture); } } + + /// + /// Looks up a localized string similar to Это бот для упрощения поиска работы на HH.ru. + /// + public static string Info { + get { + return ResourceManager.GetString("Info", resourceCulture); + } + } } } diff --git a/JOBot.TClient/TextResource.resx b/JOBot.TClient/TextResource.resx index 75fc37f..61b671f 100644 --- a/JOBot.TClient/TextResource.resx +++ b/JOBot.TClient/TextResource.resx @@ -28,4 +28,7 @@ Команда не найдена, попробуйте что-то другое + + Это бот для упрощения поиска работы на HH.ru + \ No newline at end of file -- 2.47.1 From 6e16c5831e9ca8d40f28049f9c388d3a13a7aa3a Mon Sep 17 00:00:00 2001 From: Lisoveliy Date: Sat, 12 Jul 2025 02:02:03 +0300 Subject: [PATCH 3/3] chore: cleanup --- .../Buttons/EulaAgreementButtonCommand.cs | 3 --- .../Commands/{ => Commands}/InfoCommand.cs | 2 +- .../Commands/{ => Commands}/MenuCommand.cs | 2 +- .../Commands/{ => Commands}/StartCommand.cs | 4 +--- .../Commands/IAuthorizedTelegramCommand.cs | 2 ++ .../HostedServices/BotBackgroundService.cs | 20 ++++++++++++------- JOBot.TClient/DependencyInjection.cs | 2 +- 7 files changed, 19 insertions(+), 16 deletions(-) rename JOBot.TClient/Commands/{ => Commands}/InfoCommand.cs (89%) rename JOBot.TClient/Commands/{ => Commands}/MenuCommand.cs (87%) rename JOBot.TClient/Commands/{ => Commands}/StartCommand.cs (73%) diff --git a/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs b/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs index 4f1c6d3..87d6368 100644 --- a/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs +++ b/JOBot.TClient/Commands/Buttons/EulaAgreementButtonCommand.cs @@ -12,9 +12,6 @@ public class EulaAgreementButtonCommand(PrepareUserState prepareUserState) : IAu { public async Task ExecuteAsync(Update update, GetUserResponse user, CancellationToken ct) { - if (update.Type != UpdateType.Message || update.Message?.From == null) - return; - await prepareUserState.AcceptEula(update, ct); } } \ No newline at end of file diff --git a/JOBot.TClient/Commands/InfoCommand.cs b/JOBot.TClient/Commands/Commands/InfoCommand.cs similarity index 89% rename from JOBot.TClient/Commands/InfoCommand.cs rename to JOBot.TClient/Commands/Commands/InfoCommand.cs index 084fb2a..45bb655 100644 --- a/JOBot.TClient/Commands/InfoCommand.cs +++ b/JOBot.TClient/Commands/Commands/InfoCommand.cs @@ -1,7 +1,7 @@ using Telegram.Bot; using Telegram.Bot.Types; -namespace JOBot.TClient.Commands; +namespace JOBot.TClient.Commands.Commands; public class InfoCommand(ITelegramBotClient bot) : ITelegramCommand { diff --git a/JOBot.TClient/Commands/MenuCommand.cs b/JOBot.TClient/Commands/Commands/MenuCommand.cs similarity index 87% rename from JOBot.TClient/Commands/MenuCommand.cs rename to JOBot.TClient/Commands/Commands/MenuCommand.cs index b5b7393..acce938 100644 --- a/JOBot.TClient/Commands/MenuCommand.cs +++ b/JOBot.TClient/Commands/Commands/MenuCommand.cs @@ -2,7 +2,7 @@ using JOBot.Proto; using JOBot.TClient.Services; using Telegram.Bot.Types; -namespace JOBot.TClient.Commands; +namespace JOBot.TClient.Commands.Commands; public class MenuCommand(MenuService menuService) : IAuthorizedTelegramCommand { diff --git a/JOBot.TClient/Commands/StartCommand.cs b/JOBot.TClient/Commands/Commands/StartCommand.cs similarity index 73% rename from JOBot.TClient/Commands/StartCommand.cs rename to JOBot.TClient/Commands/Commands/StartCommand.cs index b2f5bb4..e8c11f8 100644 --- a/JOBot.TClient/Commands/StartCommand.cs +++ b/JOBot.TClient/Commands/Commands/StartCommand.cs @@ -1,9 +1,7 @@ -using JOBot.TClient.Commands.Buttons; using JOBot.TClient.Statements; using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; -namespace JOBot.TClient.Commands; +namespace JOBot.TClient.Commands.Commands; public class StartCommand(PrepareUserState prepareUserState) : ITelegramCommand { diff --git a/JOBot.TClient/Commands/IAuthorizedTelegramCommand.cs b/JOBot.TClient/Commands/IAuthorizedTelegramCommand.cs index 4d37881..7d0d035 100644 --- a/JOBot.TClient/Commands/IAuthorizedTelegramCommand.cs +++ b/JOBot.TClient/Commands/IAuthorizedTelegramCommand.cs @@ -8,6 +8,8 @@ public interface IAuthorizedTelegramCommand : ITelegramCommand { public Task ExecuteAsync(Update update, GetUserResponse user, CancellationToken ct); + /// Throws if you try to use ITelegramCommand.ExecuteAsync + /// instead of IAuthorizedTelegramCommand.ExecuteAsync Task ITelegramCommand.ExecuteAsync(Update update, CancellationToken ct) { throw new UnauthorizedAccessException("You do not have permission to access this command."); diff --git a/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs b/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs index 82264e8..87e63ff 100644 --- a/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs +++ b/JOBot.TClient/Core/HostedServices/BotBackgroundService.cs @@ -1,5 +1,6 @@ using JOBot.TClient.Commands; using JOBot.TClient.Commands.Buttons; +using JOBot.TClient.Commands.Commands; using JOBot.TClient.Infrastructure.Attributes.Authorization; using JOBot.TClient.Infrastructure.Extensions; using JOBot.TClient.Services; @@ -37,15 +38,15 @@ public sealed class BotBackgroundService( ["/start"] = scope.ServiceProvider.GetRequiredService(), ["/menu"] = scope.ServiceProvider.GetRequiredService(), ["/info"] = scope.ServiceProvider.GetRequiredService(), - + //Buttons [ButtonResource.EULAAgrement] = scope.ServiceProvider.GetRequiredService(), }; - if (update.Message?.Text is { } text && update.Message?.From != null) + if (update.Message is { Text: { } text, From: not null }) { - var user = await userService.GetUser(update, ct); //Check user for existance - + var user = await userService.GetUser(update, ct); //Проверка существования пользователя + if (user == null) { await commands["/start"].ExecuteAsync(update, ct); @@ -56,18 +57,23 @@ public sealed class BotBackgroundService( { if (command is IAuthorizedTelegramCommand authorizedTelegramCommand) { - var attribute = Attribute.GetCustomAttribute(command.GetType(), typeof(AcceptNotPreparedAttribute)); + var attribute = Attribute.GetCustomAttribute( + command.GetType(), + typeof(AcceptNotPreparedAttribute)); if (!user.IsPrepared() && attribute is not AcceptNotPreparedAttribute) { - await commands["/start"].ExecuteAsync(update, ct); //заставляем пользователя завершить регистрацию + await commands["/start"].ExecuteAsync(update, ct); + //заставляем пользователя завершить регистрацию return; } + await authorizedTelegramCommand.ExecuteAsync(update, user, ct); } else await command.ExecuteAsync(update, ct); + return; } - + await bot.SendMessage(update.Message.From.Id, TextResource.CommandNotFound, cancellationToken: ct); } } diff --git a/JOBot.TClient/DependencyInjection.cs b/JOBot.TClient/DependencyInjection.cs index 71a7e93..153be1b 100644 --- a/JOBot.TClient/DependencyInjection.cs +++ b/JOBot.TClient/DependencyInjection.cs @@ -1,8 +1,8 @@ using Grpc.Core; using Grpc.Net.Client; using JOBot.Proto; -using JOBot.TClient.Commands; using JOBot.TClient.Commands.Buttons; +using JOBot.TClient.Commands.Commands; using JOBot.TClient.Core.HostedServices; using JOBot.TClient.Services; using JOBot.TClient.Statements; -- 2.47.1