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