From 8f6dbbee8c19338a4f3c934c0ebddedd583e5d50 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 25 Apr 2024 16:04:28 +0700 Subject: [PATCH 01/37] basic upgrade, part one: runtime to 8, libs updated, some code changed to accomodate API updates --- WebToTelegramCore/Services/TelegramBotService.cs | 5 ++--- WebToTelegramCore/WebToTelegramCore.csproj | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/WebToTelegramCore/Services/TelegramBotService.cs b/WebToTelegramCore/Services/TelegramBotService.cs index 188d63a..26477ba 100644 --- a/WebToTelegramCore/Services/TelegramBotService.cs +++ b/WebToTelegramCore/Services/TelegramBotService.cs @@ -3,7 +3,6 @@ using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -using Telegram.Bot.Types.InputFiles; using WebToTelegramCore.Data; using WebToTelegramCore.Interfaces; using WebToTelegramCore.Options; @@ -73,7 +72,7 @@ public async Task Send(long accountId, string message, bool silent = false, Mess var chatId = new ChatId(accountId); await _client.SendTextMessageAsync(chatId, message, - ResolveRequestParseMode(parsingType), disableWebPagePreview: true, + parseMode: ResolveRequestParseMode(parsingType), disableWebPagePreview: true, disableNotification: silent); } @@ -86,7 +85,7 @@ await _client.SendTextMessageAsync(chatId, message, public async Task SendSticker(long accountId, string stickerFileId, bool silent = false) { var chatId = new ChatId(accountId); - var sticker = new InputOnlineFile(stickerFileId); + var sticker = new InputFileId(stickerFileId); await _client.SendStickerAsync(chatId, sticker, disableNotification: silent); } diff --git a/WebToTelegramCore/WebToTelegramCore.csproj b/WebToTelegramCore/WebToTelegramCore.csproj index 79f8780..73504fd 100644 --- a/WebToTelegramCore/WebToTelegramCore.csproj +++ b/WebToTelegramCore/WebToTelegramCore.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 2.1.0.0 bnfour Dotnet Telegram forwarder @@ -9,9 +9,9 @@ - - - + + + From 70a302f29c91215e36c3ecd59978b1442985101b Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 25 Apr 2024 16:06:54 +0700 Subject: [PATCH 02/37] basic upgrade, part two: updated the year this thing was last updated to current year --- LICENSE | 2 +- WebToTelegramCore/Resources/Locale.cs | 2 +- WebToTelegramCore/WebToTelegramCore.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index b6aa777..bebce21 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018, 2020-2023 bnfour +Copyright (c) 2018, 2020-2024 bnfour Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/WebToTelegramCore/Resources/Locale.cs b/WebToTelegramCore/Resources/Locale.cs index 0b177f6..c1c9fab 100644 --- a/WebToTelegramCore/Resources/Locale.cs +++ b/WebToTelegramCore/Resources/Locale.cs @@ -20,7 +20,7 @@ public static class Locale **Dotnet Telegram forwarder** v {0}\. [Open\-source\!](https://github.com/bnfour/dotnet-telegram-forwarder) - by bnfour, 2018, 2020\-2023\. + by bnfour, 2018, 2020\-2024\. """; /// diff --git a/WebToTelegramCore/WebToTelegramCore.csproj b/WebToTelegramCore/WebToTelegramCore.csproj index 73504fd..6468054 100644 --- a/WebToTelegramCore/WebToTelegramCore.csproj +++ b/WebToTelegramCore/WebToTelegramCore.csproj @@ -5,7 +5,7 @@ 2.1.0.0 bnfour Dotnet Telegram forwarder - © 2018, 2020–2023, bnfour + © 2018, 2020–2024, bnfour From 8bd1d2b355bddd600583e880a0b8810931738793 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 25 Apr 2024 16:11:27 +0700 Subject: [PATCH 03/37] basic upgrade, part three: the great namespace change "WebToTelegramCore" -> "Bnfour.WebToTelegramCore" not like it matters, but still --- WebToTelegramCore/BotCommands/AboutCommand.cs | 8 ++++---- WebToTelegramCore/BotCommands/BotCommandBase.cs | 10 +++++----- WebToTelegramCore/BotCommands/CancelCommand.cs | 10 +++++----- WebToTelegramCore/BotCommands/ConfirmCommand.cs | 10 +++++----- .../BotCommands/ConfirmationCommandBase.cs | 10 +++++----- WebToTelegramCore/BotCommands/CreateCommand.cs | 8 ++++---- WebToTelegramCore/BotCommands/DeleteCommand.cs | 10 +++++----- WebToTelegramCore/BotCommands/DirectiveCommand.cs | 6 +++--- .../BotCommands/GuestOnlyCommandBase.cs | 8 ++++---- WebToTelegramCore/BotCommands/HelpCommand.cs | 8 ++++---- WebToTelegramCore/BotCommands/RegenerateCommand.cs | 10 +++++----- WebToTelegramCore/BotCommands/StartCommand.cs | 8 ++++---- WebToTelegramCore/BotCommands/TokenCommand.cs | 10 +++++----- WebToTelegramCore/BotCommands/UserOnlyCommandBase.cs | 8 ++++---- .../Controllers/TelegramApiController.cs | 4 ++-- WebToTelegramCore/Controllers/WebApiController.cs | 8 ++++---- WebToTelegramCore/Data/MessageParsingType.cs | 2 +- WebToTelegramCore/Data/RecordState.cs | 2 +- .../Exceptions/BandwidthExceededException.cs | 2 +- .../Exceptions/TokenNotFoundException.cs | 2 +- .../Helpers/TelegramMarkdownFormatter.cs | 2 +- WebToTelegramCore/Interfaces/IBotCommand.cs | 4 ++-- WebToTelegramCore/Interfaces/IOwnApiService.cs | 4 ++-- WebToTelegramCore/Interfaces/IRecordService.cs | 4 ++-- WebToTelegramCore/Interfaces/ITelegramApiService.cs | 2 +- WebToTelegramCore/Interfaces/ITelegramBotService.cs | 4 ++-- .../Interfaces/ITokenGeneratorService.cs | 2 +- WebToTelegramCore/Models/Record.cs | 4 ++-- WebToTelegramCore/Models/Request.cs | 4 ++-- WebToTelegramCore/Options/BandwidthOptions.cs | 2 +- WebToTelegramCore/Options/CommonOptions.cs | 2 +- WebToTelegramCore/Program.cs | 4 ++-- WebToTelegramCore/RecordContext.cs | 4 ++-- WebToTelegramCore/Resources/Locale.cs | 2 +- WebToTelegramCore/Services/OwnApiService.cs | 8 ++++---- WebToTelegramCore/Services/RecordService.cs | 10 +++++----- WebToTelegramCore/Services/TelegramApiService.cs | 12 ++++++------ WebToTelegramCore/Services/TelegramBotService.cs | 8 ++++---- WebToTelegramCore/Services/TokenGeneratorService.cs | 4 ++-- WebToTelegramCore/Startup.cs | 8 ++++---- WebToTelegramCore/WebToTelegramCore.csproj | 1 + 41 files changed, 120 insertions(+), 119 deletions(-) diff --git a/WebToTelegramCore/BotCommands/AboutCommand.cs b/WebToTelegramCore/BotCommands/AboutCommand.cs index 6747417..fff3b91 100644 --- a/WebToTelegramCore/BotCommands/AboutCommand.cs +++ b/WebToTelegramCore/BotCommands/AboutCommand.cs @@ -1,10 +1,10 @@ using System; using System.Reflection; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Class that handles /about command that shows general info about the bot. diff --git a/WebToTelegramCore/BotCommands/BotCommandBase.cs b/WebToTelegramCore/BotCommands/BotCommandBase.cs index 924dd31..cd12f1c 100644 --- a/WebToTelegramCore/BotCommands/BotCommandBase.cs +++ b/WebToTelegramCore/BotCommands/BotCommandBase.cs @@ -1,9 +1,9 @@ -using WebToTelegramCore.Data; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Base class for commands which are used to anything but to confirm or cancel diff --git a/WebToTelegramCore/BotCommands/CancelCommand.cs b/WebToTelegramCore/BotCommands/CancelCommand.cs index fb0e503..7acc8ce 100644 --- a/WebToTelegramCore/BotCommands/CancelCommand.cs +++ b/WebToTelegramCore/BotCommands/CancelCommand.cs @@ -1,9 +1,9 @@ -using WebToTelegramCore.Data; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Class that implements /cancel command to cancel pending destructive diff --git a/WebToTelegramCore/BotCommands/ConfirmCommand.cs b/WebToTelegramCore/BotCommands/ConfirmCommand.cs index 42f224b..d422c57 100644 --- a/WebToTelegramCore/BotCommands/ConfirmCommand.cs +++ b/WebToTelegramCore/BotCommands/ConfirmCommand.cs @@ -1,10 +1,10 @@ using System; -using WebToTelegramCore.Data; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Class that implements /confirm command which either deletes user's token or diff --git a/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs b/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs index 986513a..deed2b3 100644 --- a/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs +++ b/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs @@ -1,9 +1,9 @@ -using WebToTelegramCore.Data; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Base class for confirmation commands that is used only when diff --git a/WebToTelegramCore/BotCommands/CreateCommand.cs b/WebToTelegramCore/BotCommands/CreateCommand.cs index 0c96faa..0f84f19 100644 --- a/WebToTelegramCore/BotCommands/CreateCommand.cs +++ b/WebToTelegramCore/BotCommands/CreateCommand.cs @@ -1,9 +1,9 @@ using System; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// /create command, allows user to create a token and start using the bot. diff --git a/WebToTelegramCore/BotCommands/DeleteCommand.cs b/WebToTelegramCore/BotCommands/DeleteCommand.cs index 74eddad..c4fa9a2 100644 --- a/WebToTelegramCore/BotCommands/DeleteCommand.cs +++ b/WebToTelegramCore/BotCommands/DeleteCommand.cs @@ -1,9 +1,9 @@ -using WebToTelegramCore.Data; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Represents command /delete that marks user account to deletion than must be diff --git a/WebToTelegramCore/BotCommands/DirectiveCommand.cs b/WebToTelegramCore/BotCommands/DirectiveCommand.cs index d8390e1..5f3f3d3 100644 --- a/WebToTelegramCore/BotCommands/DirectiveCommand.cs +++ b/WebToTelegramCore/BotCommands/DirectiveCommand.cs @@ -1,7 +1,7 @@ -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Class for undocumented easter egg /directive command. diff --git a/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs b/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs index 8e6ca04..7fc8815 100644 --- a/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs +++ b/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs @@ -1,8 +1,8 @@ -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Base class for commands that are both not confirmation commands and diff --git a/WebToTelegramCore/BotCommands/HelpCommand.cs b/WebToTelegramCore/BotCommands/HelpCommand.cs index f699884..63c5b61 100644 --- a/WebToTelegramCore/BotCommands/HelpCommand.cs +++ b/WebToTelegramCore/BotCommands/HelpCommand.cs @@ -1,8 +1,8 @@ -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Class that implements /help command that should spew out some support text. diff --git a/WebToTelegramCore/BotCommands/RegenerateCommand.cs b/WebToTelegramCore/BotCommands/RegenerateCommand.cs index b5e6931..fc88ff3 100644 --- a/WebToTelegramCore/BotCommands/RegenerateCommand.cs +++ b/WebToTelegramCore/BotCommands/RegenerateCommand.cs @@ -1,9 +1,9 @@ -using WebToTelegramCore.Data; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Represents command /regenerate that marks user account to regenerate token. diff --git a/WebToTelegramCore/BotCommands/StartCommand.cs b/WebToTelegramCore/BotCommands/StartCommand.cs index e98947f..68b29fc 100644 --- a/WebToTelegramCore/BotCommands/StartCommand.cs +++ b/WebToTelegramCore/BotCommands/StartCommand.cs @@ -1,8 +1,8 @@ -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Class that implements /start command. diff --git a/WebToTelegramCore/BotCommands/TokenCommand.cs b/WebToTelegramCore/BotCommands/TokenCommand.cs index a0f2278..d921654 100644 --- a/WebToTelegramCore/BotCommands/TokenCommand.cs +++ b/WebToTelegramCore/BotCommands/TokenCommand.cs @@ -1,10 +1,10 @@ using System; -using WebToTelegramCore.Helpers; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Helpers; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// /token command. Displays user's token and usage hint. diff --git a/WebToTelegramCore/BotCommands/UserOnlyCommandBase.cs b/WebToTelegramCore/BotCommands/UserOnlyCommandBase.cs index 90c038a..cdae67d 100644 --- a/WebToTelegramCore/BotCommands/UserOnlyCommandBase.cs +++ b/WebToTelegramCore/BotCommands/UserOnlyCommandBase.cs @@ -1,8 +1,8 @@ -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands { /// /// Base class for commands that are both not confirmation commands and diff --git a/WebToTelegramCore/Controllers/TelegramApiController.cs b/WebToTelegramCore/Controllers/TelegramApiController.cs index 0d61d5f..1648541 100644 --- a/WebToTelegramCore/Controllers/TelegramApiController.cs +++ b/WebToTelegramCore/Controllers/TelegramApiController.cs @@ -2,9 +2,9 @@ using System.Net; using System.Threading.Tasks; using Telegram.Bot.Types; -using WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Interfaces; -namespace WebToTelegramCore.Controllers +namespace Bnfour.WebToTelegramCore.Controllers { /// /// Controller that handles both web API and telegram API calls, diff --git a/WebToTelegramCore/Controllers/WebApiController.cs b/WebToTelegramCore/Controllers/WebApiController.cs index 61b8825..60e5216 100644 --- a/WebToTelegramCore/Controllers/WebApiController.cs +++ b/WebToTelegramCore/Controllers/WebApiController.cs @@ -1,11 +1,11 @@ using Microsoft.AspNetCore.Mvc; using System.Net; using System.Threading.Tasks; -using WebToTelegramCore.Exceptions; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Exceptions; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; -namespace WebToTelegramCore.Controllers +namespace Bnfour.WebToTelegramCore.Controllers { public class WebApiController : Controller { diff --git a/WebToTelegramCore/Data/MessageParsingType.cs b/WebToTelegramCore/Data/MessageParsingType.cs index 741c894..cb8375c 100644 --- a/WebToTelegramCore/Data/MessageParsingType.cs +++ b/WebToTelegramCore/Data/MessageParsingType.cs @@ -1,4 +1,4 @@ -namespace WebToTelegramCore.Data +namespace Bnfour.WebToTelegramCore.Data { /// /// Represents available parsing modes for the user message diff --git a/WebToTelegramCore/Data/RecordState.cs b/WebToTelegramCore/Data/RecordState.cs index 5083822..67eac3f 100644 --- a/WebToTelegramCore/Data/RecordState.cs +++ b/WebToTelegramCore/Data/RecordState.cs @@ -1,4 +1,4 @@ -namespace WebToTelegramCore.Data +namespace Bnfour.WebToTelegramCore.Data { /// /// Represents states a Record can be in. Used to keep track and to confirm diff --git a/WebToTelegramCore/Exceptions/BandwidthExceededException.cs b/WebToTelegramCore/Exceptions/BandwidthExceededException.cs index 521a79d..2aa265a 100644 --- a/WebToTelegramCore/Exceptions/BandwidthExceededException.cs +++ b/WebToTelegramCore/Exceptions/BandwidthExceededException.cs @@ -1,6 +1,6 @@ using System; -namespace WebToTelegramCore.Exceptions +namespace Bnfour.WebToTelegramCore.Exceptions { /// /// Exception that is thrown by diff --git a/WebToTelegramCore/Exceptions/TokenNotFoundException.cs b/WebToTelegramCore/Exceptions/TokenNotFoundException.cs index 1372c67..9ba0ed6 100644 --- a/WebToTelegramCore/Exceptions/TokenNotFoundException.cs +++ b/WebToTelegramCore/Exceptions/TokenNotFoundException.cs @@ -1,6 +1,6 @@ using System; -namespace WebToTelegramCore.Exceptions +namespace Bnfour.WebToTelegramCore.Exceptions { /// /// Exception that is thrown by diff --git a/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs b/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs index b4213ea..1bf6cc1 100644 --- a/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs +++ b/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs @@ -1,4 +1,4 @@ -namespace WebToTelegramCore.Helpers +namespace Bnfour.WebToTelegramCore.Helpers { /// /// Static helper to make not hardcoded texts suitable for sending. diff --git a/WebToTelegramCore/Interfaces/IBotCommand.cs b/WebToTelegramCore/Interfaces/IBotCommand.cs index a9ed703..2f109e3 100644 --- a/WebToTelegramCore/Interfaces/IBotCommand.cs +++ b/WebToTelegramCore/Interfaces/IBotCommand.cs @@ -1,6 +1,6 @@ -using WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Models; -namespace WebToTelegramCore.Interfaces +namespace Bnfour.WebToTelegramCore.Interfaces { /// /// Interface to implement various bot commands without arguments. diff --git a/WebToTelegramCore/Interfaces/IOwnApiService.cs b/WebToTelegramCore/Interfaces/IOwnApiService.cs index f8d5150..57a468e 100644 --- a/WebToTelegramCore/Interfaces/IOwnApiService.cs +++ b/WebToTelegramCore/Interfaces/IOwnApiService.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; -using WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Models; -namespace WebToTelegramCore.Interfaces +namespace Bnfour.WebToTelegramCore.Interfaces { /// /// Interface to implement non-Telegram web API handling. diff --git a/WebToTelegramCore/Interfaces/IRecordService.cs b/WebToTelegramCore/Interfaces/IRecordService.cs index a2275d5..73afb89 100644 --- a/WebToTelegramCore/Interfaces/IRecordService.cs +++ b/WebToTelegramCore/Interfaces/IRecordService.cs @@ -1,6 +1,6 @@ -using WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Models; -namespace WebToTelegramCore.Interfaces +namespace Bnfour.WebToTelegramCore.Interfaces { /// /// Interface that makes managing records look easy for its users. diff --git a/WebToTelegramCore/Interfaces/ITelegramApiService.cs b/WebToTelegramCore/Interfaces/ITelegramApiService.cs index a7a0aea..5efb84d 100644 --- a/WebToTelegramCore/Interfaces/ITelegramApiService.cs +++ b/WebToTelegramCore/Interfaces/ITelegramApiService.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using Update = Telegram.Bot.Types.Update; -namespace WebToTelegramCore.Interfaces +namespace Bnfour.WebToTelegramCore.Interfaces { /// /// Interface to implement Telegram webhook handling. diff --git a/WebToTelegramCore/Interfaces/ITelegramBotService.cs b/WebToTelegramCore/Interfaces/ITelegramBotService.cs index f64468c..60546fe 100644 --- a/WebToTelegramCore/Interfaces/ITelegramBotService.cs +++ b/WebToTelegramCore/Interfaces/ITelegramBotService.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; -using WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Data; -namespace WebToTelegramCore.Interfaces +namespace Bnfour.WebToTelegramCore.Interfaces { /// /// Interface that exposes greatly simplified version of Telegram bot API. diff --git a/WebToTelegramCore/Interfaces/ITokenGeneratorService.cs b/WebToTelegramCore/Interfaces/ITokenGeneratorService.cs index 3fabe6e..2449d9c 100644 --- a/WebToTelegramCore/Interfaces/ITokenGeneratorService.cs +++ b/WebToTelegramCore/Interfaces/ITokenGeneratorService.cs @@ -1,4 +1,4 @@ -namespace WebToTelegramCore.Interfaces +namespace Bnfour.WebToTelegramCore.Interfaces { /// /// Interface to services that generate auth tokens. diff --git a/WebToTelegramCore/Models/Record.cs b/WebToTelegramCore/Models/Record.cs index f1d6cee..8a45859 100644 --- a/WebToTelegramCore/Models/Record.cs +++ b/WebToTelegramCore/Models/Record.cs @@ -1,7 +1,7 @@ using System; -using WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Data; -namespace WebToTelegramCore.Models +namespace Bnfour.WebToTelegramCore.Models { /// /// Internal representation of Token - ID relationship with additional diff --git a/WebToTelegramCore/Models/Request.cs b/WebToTelegramCore/Models/Request.cs index 81f15e8..d556609 100644 --- a/WebToTelegramCore/Models/Request.cs +++ b/WebToTelegramCore/Models/Request.cs @@ -1,9 +1,9 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using System.ComponentModel.DataAnnotations; -using WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Data; -namespace WebToTelegramCore.Models +namespace Bnfour.WebToTelegramCore.Models { /// /// Represents user's request to our web API. diff --git a/WebToTelegramCore/Options/BandwidthOptions.cs b/WebToTelegramCore/Options/BandwidthOptions.cs index 369450b..dea1812 100644 --- a/WebToTelegramCore/Options/BandwidthOptions.cs +++ b/WebToTelegramCore/Options/BandwidthOptions.cs @@ -1,4 +1,4 @@ -namespace WebToTelegramCore.Options +namespace Bnfour.WebToTelegramCore.Options { /// /// Class that represents app's setting related to message diff --git a/WebToTelegramCore/Options/CommonOptions.cs b/WebToTelegramCore/Options/CommonOptions.cs index 01afc83..d68e677 100644 --- a/WebToTelegramCore/Options/CommonOptions.cs +++ b/WebToTelegramCore/Options/CommonOptions.cs @@ -1,4 +1,4 @@ -namespace WebToTelegramCore.Options +namespace Bnfour.WebToTelegramCore.Options { /// /// Represents application-wide options. diff --git a/WebToTelegramCore/Program.cs b/WebToTelegramCore/Program.cs index 01b4068..dc7062f 100644 --- a/WebToTelegramCore/Program.cs +++ b/WebToTelegramCore/Program.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using System; -using WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Interfaces; -namespace WebToTelegramCore +namespace Bnfour.WebToTelegramCore { public class Program { diff --git a/WebToTelegramCore/RecordContext.cs b/WebToTelegramCore/RecordContext.cs index 0c0e154..46ce64d 100644 --- a/WebToTelegramCore/RecordContext.cs +++ b/WebToTelegramCore/RecordContext.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; -using WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Models; -namespace WebToTelegramCore +namespace Bnfour.WebToTelegramCore { /// /// Class that encapsulates Records for EF usage. diff --git a/WebToTelegramCore/Resources/Locale.cs b/WebToTelegramCore/Resources/Locale.cs index c1c9fab..04f90dc 100644 --- a/WebToTelegramCore/Resources/Locale.cs +++ b/WebToTelegramCore/Resources/Locale.cs @@ -1,4 +1,4 @@ -namespace WebToTelegramCore.Resources +namespace Bnfour.WebToTelegramCore.Resources { /// /// Class that holds all customizable string the bot may reply with. diff --git a/WebToTelegramCore/Services/OwnApiService.cs b/WebToTelegramCore/Services/OwnApiService.cs index cadb73e..c6e1d33 100644 --- a/WebToTelegramCore/Services/OwnApiService.cs +++ b/WebToTelegramCore/Services/OwnApiService.cs @@ -1,9 +1,9 @@ using System.Threading.Tasks; -using WebToTelegramCore.Exceptions; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Exceptions; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; -namespace WebToTelegramCore.Services +namespace Bnfour.WebToTelegramCore.Services { /// /// Class that handles web API calls. diff --git a/WebToTelegramCore/Services/RecordService.cs b/WebToTelegramCore/Services/RecordService.cs index 75ce038..060131d 100644 --- a/WebToTelegramCore/Services/RecordService.cs +++ b/WebToTelegramCore/Services/RecordService.cs @@ -1,11 +1,11 @@ using Microsoft.Extensions.Options; using System; -using WebToTelegramCore.Data; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Options; +using Bnfour.WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Options; -namespace WebToTelegramCore.Services +namespace Bnfour.WebToTelegramCore.Services { /// /// Class that makes managing Records look easy for its users. diff --git a/WebToTelegramCore/Services/TelegramApiService.cs b/WebToTelegramCore/Services/TelegramApiService.cs index dfb9363..1ed8031 100644 --- a/WebToTelegramCore/Services/TelegramApiService.cs +++ b/WebToTelegramCore/Services/TelegramApiService.cs @@ -6,13 +6,13 @@ using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -using WebToTelegramCore.BotCommands; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Models; -using WebToTelegramCore.Options; -using WebToTelegramCore.Resources; +using Bnfour.WebToTelegramCore.BotCommands; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Options; +using Bnfour.WebToTelegramCore.Resources; -namespace WebToTelegramCore.Services +namespace Bnfour.WebToTelegramCore.Services { /// /// Class that implements handling webhook updates. diff --git a/WebToTelegramCore/Services/TelegramBotService.cs b/WebToTelegramCore/Services/TelegramBotService.cs index 26477ba..e48de2e 100644 --- a/WebToTelegramCore/Services/TelegramBotService.cs +++ b/WebToTelegramCore/Services/TelegramBotService.cs @@ -3,11 +3,11 @@ using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -using WebToTelegramCore.Data; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Options; +using Bnfour.WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Options; -namespace WebToTelegramCore.Services +namespace Bnfour.WebToTelegramCore.Services { /// /// Provides realization to ITelegramBotService. Encapsulates Telegram.Bot API diff --git a/WebToTelegramCore/Services/TokenGeneratorService.cs b/WebToTelegramCore/Services/TokenGeneratorService.cs index 006d63c..7c9219c 100644 --- a/WebToTelegramCore/Services/TokenGeneratorService.cs +++ b/WebToTelegramCore/Services/TokenGeneratorService.cs @@ -2,9 +2,9 @@ using System.Security.Cryptography; using System.Linq; using System.Text; -using WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Interfaces; -namespace WebToTelegramCore.Services +namespace Bnfour.WebToTelegramCore.Services { /// /// Class that actually generates tokens. diff --git a/WebToTelegramCore/Startup.cs b/WebToTelegramCore/Startup.cs index b2ba015..e60896f 100644 --- a/WebToTelegramCore/Startup.cs +++ b/WebToTelegramCore/Startup.cs @@ -1,11 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using WebToTelegramCore.Interfaces; -using WebToTelegramCore.Options; -using WebToTelegramCore.Services; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Options; +using Bnfour.WebToTelegramCore.Services; -namespace WebToTelegramCore +namespace Bnfour.WebToTelegramCore { public class Startup { diff --git a/WebToTelegramCore/WebToTelegramCore.csproj b/WebToTelegramCore/WebToTelegramCore.csproj index 6468054..2780a80 100644 --- a/WebToTelegramCore/WebToTelegramCore.csproj +++ b/WebToTelegramCore/WebToTelegramCore.csproj @@ -6,6 +6,7 @@ bnfour Dotnet Telegram forwarder © 2018, 2020–2024, bnfour + Bnfour.WebToTelegramCore From 7b8f7bbfbf5e142594ef102bb799fa51ea349be6 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 25 Apr 2024 16:24:12 +0700 Subject: [PATCH 04/37] one rather lager commit unfortunately: - split interfaces for webhooks and messaging -- still served by a single service, but it is no longer a singleton - comment fixes, new namespaces format for affected files --- .../Interfaces/ITelegramBotService.cs | 51 ------ .../Interfaces/ITelegramMessageService.cs | 36 ++++ .../Interfaces/ITelegramWebhookService.cs | 24 +++ WebToTelegramCore/Program.cs | 92 ++++++---- WebToTelegramCore/Services/OwnApiService.cs | 4 +- .../Services/TelegramApiService.cs | 4 +- .../Services/TelegramBotService.cs | 171 +++++++++--------- WebToTelegramCore/Startup.cs | 50 ++--- 8 files changed, 226 insertions(+), 206 deletions(-) delete mode 100644 WebToTelegramCore/Interfaces/ITelegramBotService.cs create mode 100644 WebToTelegramCore/Interfaces/ITelegramMessageService.cs create mode 100644 WebToTelegramCore/Interfaces/ITelegramWebhookService.cs diff --git a/WebToTelegramCore/Interfaces/ITelegramBotService.cs b/WebToTelegramCore/Interfaces/ITelegramBotService.cs deleted file mode 100644 index 60546fe..0000000 --- a/WebToTelegramCore/Interfaces/ITelegramBotService.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Threading.Tasks; -using Bnfour.WebToTelegramCore.Data; - -namespace Bnfour.WebToTelegramCore.Interfaces -{ - /// - /// Interface that exposes greatly simplified version of Telegram bot API. - /// The only method allows sending of text messages. - /// - public interface ITelegramBotService - { - /// - /// Sends text message. Markdown formatting should be supported. - /// - /// ID of account to send message to. - /// Text of the message. - /// Flag to set whether to suppress the notification. - /// Like the API defaults to false for bot responses, which are sent immediately after user's message. - /// Formatting type used in the message. - /// Unlike the API, defaults for Markdown for bot responses, which do use some formatting. - Task Send(long accountId, string message, bool silent = false, MessageParsingType parsingType = MessageParsingType.Markdown); - - /// - /// Method to send an arbitrary sticker on behalf of the bot. - /// - /// ID of the account to send to. - /// ID of the sticker to send. - Task SendSticker(long accountId, string stickerFileId, bool silent = false); - - /// - /// Sends a predefined sticker. Used as an easter egg with a 5% chance - /// of message on unknown user input to be the sticker instead of text. - /// - /// ID of account to send sticker to. - Task SendTheSticker(long accountId); - - /// - /// Method to manage external webhook for the Telegram API. - /// Part one: called to set the webhook on application start. - /// - Task SetExternalWebhook(); - - /// - /// Method to manage external webhook for the Telegram API. - /// Part two: called to remove the webhook gracefully on application exit. - /// Not that it makes much sense in an app designed to be run 24/7, but is - /// nice to have nonetheless. - /// - Task ClearExternalWebhook(); - } -} diff --git a/WebToTelegramCore/Interfaces/ITelegramMessageService.cs b/WebToTelegramCore/Interfaces/ITelegramMessageService.cs new file mode 100644 index 0000000..cb1cc1f --- /dev/null +++ b/WebToTelegramCore/Interfaces/ITelegramMessageService.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Bnfour.WebToTelegramCore.Data; + +namespace Bnfour.WebToTelegramCore.Interfaces; + +/// +/// Interface that exposes greatly simplified version of Telegram bot API. +/// Allows sending text messages or stickers. +/// +public interface ITelegramMessageService +{ + /// + /// Sends text message. Markdown formatting should be supported. + /// + /// ID of account to send message to. + /// Text of the message. + /// Flag to set whether to suppress the notification. + /// Like the API defaults to false for bot responses, which are sent immediately after user's message. + /// Formatting type used in the message. + /// Unlike the API, defaults for Markdown for bot responses, which do use some formatting. + Task Send(long accountId, string message, bool silent = false, MessageParsingType parsingType = MessageParsingType.Markdown); + + /// + /// Method to send an arbitrary sticker on behalf of the bot. + /// + /// ID of the account to send to. + /// ID of the sticker to send. + Task SendSticker(long accountId, string stickerFileId, bool silent = false); + + /// + /// Sends a predefined sticker. Used as an easter egg with a 5% chance + /// of message on unknown user input to be the sticker instead of text. + /// + /// ID of account to send sticker to. + Task SendTheSticker(long accountId); +} diff --git a/WebToTelegramCore/Interfaces/ITelegramWebhookService.cs b/WebToTelegramCore/Interfaces/ITelegramWebhookService.cs new file mode 100644 index 0000000..6ae43a6 --- /dev/null +++ b/WebToTelegramCore/Interfaces/ITelegramWebhookService.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace Bnfour.WebToTelegramCore.Interfaces; + +/// +/// Exposes the methods to manage the webhook for Telegram API. +/// Used on application startup and shutdown. +/// +public interface ITelegramWebhookService +{ + /// + /// Method to manage external webhook for the Telegram API. + /// Part one: called to set the webhook on application start. + /// + Task SetExternalWebhook(); + + /// + /// Method to manage external webhook for the Telegram API. + /// Part two: called to remove the webhook gracefully on application exit. + /// Not that it makes much sense in an app designed to be run 24/7, but is + /// nice to have nonetheless. + /// + Task ClearExternalWebhook(); +} diff --git a/WebToTelegramCore/Program.cs b/WebToTelegramCore/Program.cs index dc7062f..ca4a811 100644 --- a/WebToTelegramCore/Program.cs +++ b/WebToTelegramCore/Program.cs @@ -2,56 +2,68 @@ using Microsoft.AspNetCore.Hosting; using System; using Bnfour.WebToTelegramCore.Interfaces; +using Microsoft.Extensions.DependencyInjection; -namespace Bnfour.WebToTelegramCore +namespace Bnfour.WebToTelegramCore; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) + // hardcoded default, because 8080 and 8081 were already taken on my machine + int port = 8082; + + if (args.Length == 2 && args[0].Equals("--port") && int.TryParse(args[1], out int nonDefPort)) { - // hardcoded default, because 8080 and 8081 were already taken on my machine - int port = 8082; + port = nonDefPort; + } - if (args.Length == 2 && args[0].Equals("--port") && int.TryParse(args[1], out int nonDefPort)) - { - port = nonDefPort; - } + var builder = WebApplication.CreateBuilder(new WebApplicationOptions + { + // quick fix to prevent using solution folder for configuration files + // instead of built binary folder, where these files are overridden with juicy secrets + ContentRootPath = AppContext.BaseDirectory + }); - var builder = WebApplication.CreateBuilder(new WebApplicationOptions - { - // quick fix to prevent using solution folder for configuration files - // instead of built binary folder, where these files are overridden with juicy secrets - ContentRootPath = AppContext.BaseDirectory - }); - - // this is used to force the code to use database from the current (e.g output for debug) directory - // instead of using the file in the project root every launch - - // please note that this value ends with backslash, so in the connection string, - // file name goes straight after |DataDirectory|, no slashes of any kind - AppDomain.CurrentDomain.SetData("DataDirectory", AppContext.BaseDirectory); - - builder.WebHost.UseKestrel(kestrelOptions => - { - // this won't work without a SSL proxy over it anyway, - // so listening to localhost only - kestrelOptions.ListenLocalhost(port); - }); + // this is used to force the code to use database from the current (e.g output for debug) directory + // instead of using the file in the project root every launch + + // please note that this value ends with backslash, so in the connection string, + // file name goes straight after |DataDirectory|, no slashes of any kind + AppDomain.CurrentDomain.SetData("DataDirectory", AppContext.BaseDirectory); - // carrying over legacy startup-based configuration (for now?) - var startup = new Startup(builder.Configuration); - startup.ConfigureServices(builder.Services); + builder.WebHost.UseKestrel(kestrelOptions => + { + // this won't work without a SSL proxy over it anyway, + // so listening to localhost only + kestrelOptions.ListenLocalhost(port); + }); - var app = builder.Build(); - app.MapControllers(); + // carrying over legacy startup-based configuration (for now? even in 2024?) + var startup = new Startup(builder.Configuration); + startup.ConfigureServices(builder.Services); - app.Lifetime.ApplicationStarted.Register(async () => - await (app.Services.GetService(typeof(ITelegramBotService)) as ITelegramBotService).SetExternalWebhook()); + var app = builder.Build(); + app.MapControllers(); - app.Lifetime.ApplicationStopped.Register(async () => - await (app.Services.GetService(typeof(ITelegramBotService)) as ITelegramBotService).ClearExternalWebhook()); + app.Lifetime.ApplicationStarted.Register(async () => + { + using (var scope = app.Services.CreateScope()) + { + var service = scope.ServiceProvider.GetService(); + await service.SetExternalWebhook(); + } + }); - app.Run(); - } + app.Lifetime.ApplicationStopped.Register(async () => + { + using (var scope = app.Services.CreateScope()) + { + var service = scope.ServiceProvider.GetService(); + await service.ClearExternalWebhook(); + } + }); + + app.Run(); } } diff --git a/WebToTelegramCore/Services/OwnApiService.cs b/WebToTelegramCore/Services/OwnApiService.cs index c6e1d33..c337510 100644 --- a/WebToTelegramCore/Services/OwnApiService.cs +++ b/WebToTelegramCore/Services/OwnApiService.cs @@ -18,7 +18,7 @@ public class OwnApiService : IOwnApiService /// /// Field to store bot service used to send messages. /// - private readonly ITelegramBotService _bot; + private readonly ITelegramMessageService _bot; /// /// Field to store Record management service. @@ -31,7 +31,7 @@ public class OwnApiService : IOwnApiService /// Database context to use. /// Bot service to use. /// Bandwidth options. - public OwnApiService(RecordContext context, ITelegramBotService bot, IRecordService recordService) + public OwnApiService(RecordContext context, ITelegramMessageService bot, IRecordService recordService) { _context = context; _bot = bot; diff --git a/WebToTelegramCore/Services/TelegramApiService.cs b/WebToTelegramCore/Services/TelegramApiService.cs index 1ed8031..b36155f 100644 --- a/WebToTelegramCore/Services/TelegramApiService.cs +++ b/WebToTelegramCore/Services/TelegramApiService.cs @@ -32,7 +32,7 @@ public class TelegramApiService : ITelegramApiService /// /// Bot service to send messages (and sometimes stickers). /// - private readonly ITelegramBotService _bot; + private readonly ITelegramMessageService _bot; /// /// Reference to token generator service. @@ -58,7 +58,7 @@ public class TelegramApiService : ITelegramApiService /// Token generator service to use. /// Record helper service to use. public TelegramApiService(IOptions options, - RecordContext context, ITelegramBotService bot, + RecordContext context, ITelegramMessageService bot, ITokenGeneratorService generator, IRecordService recordService) { _token = options.Value.Token; diff --git a/WebToTelegramCore/Services/TelegramBotService.cs b/WebToTelegramCore/Services/TelegramBotService.cs index e48de2e..5da4cd2 100644 --- a/WebToTelegramCore/Services/TelegramBotService.cs +++ b/WebToTelegramCore/Services/TelegramBotService.cs @@ -7,106 +7,105 @@ using Bnfour.WebToTelegramCore.Interfaces; using Bnfour.WebToTelegramCore.Options; -namespace Bnfour.WebToTelegramCore.Services +namespace Bnfour.WebToTelegramCore.Services; + +/// +/// Provides realization to Telegram bot related service interfaces. +/// Encapsulates Telegram.Bot API to send messages and manage webhooks. +/// +public class TelegramBotService : ITelegramMessageService, ITelegramWebhookService { /// - /// Provides realization to ITelegramBotService. Encapsulates Telegram.Bot API - /// to send messages. + /// Field to store bot client instance. /// - public class TelegramBotService : ITelegramBotService - { - /// - /// Field to store bot client instance. - /// - private readonly TelegramBotClient _client; + private readonly TelegramBotClient _client; - /// - /// Field to store ID of the sticker to be used as an easter egg. - /// - private const string _theStickerID = "CAACAgIAAxkBAAIBLWRXhjYCsdyPBbQaJNEJlbtzpVKLAAJ6CQAC8UK_BfOoYI7o8z7PLwQ"; + /// + /// Field to store ID of the sticker to be used as an easter egg. + /// + private const string _theStickerID = "CAACAgIAAxkBAAIBLWRXhjYCsdyPBbQaJNEJlbtzpVKLAAJ6CQAC8UK_BfOoYI7o8z7PLwQ"; - /// - /// Field to store our webhook URL to be advertised to Telegram's API. - /// - private readonly string _webhookUrl; + /// + /// Field to store our webhook URL to be advertised to Telegram's API. + /// + private readonly string _webhookUrl; - /// - /// Constructor that get the options required for this service to operate. - /// - /// Options to use. - public TelegramBotService(IOptions options) - { - _client = new TelegramBotClient(options.Value.Token); - // made unclear that "api" part is needed as well, shot myself in the leg 3 years after - _webhookUrl = options.Value.ApiEndpointUrl + "/api/" + options.Value.Token; - } + /// + /// Constructor that get the options required for this service to operate. + /// + /// Options to use. + public TelegramBotService(IOptions options) + { + _client = new TelegramBotClient(options.Value.Token); + // made unclear that "api" part is needed as well, shot myself in the leg 3 years after + _webhookUrl = options.Value.ApiEndpointUrl + "/api/" + options.Value.Token; + } - /// - /// Method to manage external webhook for the Telegram API. - /// Part one: called to set the webhook on application start. - /// - public async Task SetExternalWebhook() - { - await _client.SetWebhookAsync(_webhookUrl, allowedUpdates: new[] { UpdateType.Message }); - } + /// + /// Method to manage external webhook for the Telegram API. + /// Part one: called to set the webhook on application start. + /// + public async Task SetExternalWebhook() + { + await _client.SetWebhookAsync(_webhookUrl, allowedUpdates: new[] { UpdateType.Message }); + } - /// - /// Method to manage external webhook for the Telegram API. - /// Part two: called to remove the webhook gracefully on application exit. - /// - public async Task ClearExternalWebhook() - { - await _client.DeleteWebhookAsync(); - } + /// + /// Method to manage external webhook for the Telegram API. + /// Part two: called to remove the webhook gracefully on application exit. + /// + public async Task ClearExternalWebhook() + { + await _client.DeleteWebhookAsync(); + } - /// - /// Method that is used from outside to send messages on behalf of the bot. - /// - /// ID of the account to send to. - /// Markdown-formatted message. - /// Flag to set whether to suppress the notification. - /// Formatting type used in the message. - public async Task Send(long accountId, string message, bool silent = false, MessageParsingType parsingType = MessageParsingType.Markdown) - { - // I think we have to promote account ID back to ID of chat with this bot - var chatId = new ChatId(accountId); + /// + /// Method that is used from outside to send messages on behalf of the bot. + /// + /// ID of the account to send to. + /// Markdown-formatted message. + /// Flag to set whether to suppress the notification. + /// Formatting type used in the message. + public async Task Send(long accountId, string message, bool silent = false, MessageParsingType parsingType = MessageParsingType.Markdown) + { + // I think we have to promote account ID back to ID of chat with this bot + var chatId = new ChatId(accountId); - await _client.SendTextMessageAsync(chatId, message, - parseMode: ResolveRequestParseMode(parsingType), disableWebPagePreview: true, - disableNotification: silent); - } + await _client.SendTextMessageAsync(chatId, message, + parseMode: ResolveRequestParseMode(parsingType), disableWebPagePreview: true, + disableNotification: silent); + } - /// - /// Method to send an arbitrary sticker on behalf of the bot. - /// - /// ID of the account to send to. - /// ID of the sticker to send. - /// Flag to set whether to suppress the notification. - public async Task SendSticker(long accountId, string stickerFileId, bool silent = false) - { - var chatId = new ChatId(accountId); - var sticker = new InputFileId(stickerFileId); + /// + /// Method to send an arbitrary sticker on behalf of the bot. + /// + /// ID of the account to send to. + /// ID of the sticker to send. + /// Flag to set whether to suppress the notification. + public async Task SendSticker(long accountId, string stickerFileId, bool silent = false) + { + var chatId = new ChatId(accountId); + var sticker = new InputFileId(stickerFileId); - await _client.SendStickerAsync(chatId, sticker, disableNotification: silent); - } + await _client.SendStickerAsync(chatId, sticker, disableNotification: silent); + } - /// - /// Method to send predefined sticker on behalf of the bot. - /// - /// ID of the account to send to. - public async Task SendTheSticker(long accountId) - => await SendSticker(accountId, _theStickerID); + /// + /// Method to send predefined sticker on behalf of the bot. + /// + /// ID of the account to send to. + public async Task SendTheSticker(long accountId) + => await SendSticker(accountId, _theStickerID); - private ParseMode? ResolveRequestParseMode(MessageParsingType fromRequest) + private ParseMode? ResolveRequestParseMode(MessageParsingType fromRequest) + { + return fromRequest switch { - return fromRequest switch - { - MessageParsingType.Plaintext => null, - MessageParsingType.Markdown => ParseMode.MarkdownV2, - // should never happen, but for sake of completeness, - // fall back to plaintext - _ => null - }; - } + MessageParsingType.Plaintext => null, + MessageParsingType.Markdown => ParseMode.MarkdownV2, + // should never happen, but for sake of completeness, + // fall back to plaintext + _ => null + }; } } diff --git a/WebToTelegramCore/Startup.cs b/WebToTelegramCore/Startup.cs index e60896f..af68484 100644 --- a/WebToTelegramCore/Startup.cs +++ b/WebToTelegramCore/Startup.cs @@ -5,39 +5,39 @@ using Bnfour.WebToTelegramCore.Options; using Bnfour.WebToTelegramCore.Services; -namespace Bnfour.WebToTelegramCore +namespace Bnfour.WebToTelegramCore; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - // singleton makes changes to non-db properties persistent - services.AddDbContext(options => - options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")), - ServiceLifetime.Singleton, ServiceLifetime.Singleton); + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // singleton makes changes to non-db properties persistent + services.AddDbContext(options => + options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")), + ServiceLifetime.Singleton, ServiceLifetime.Singleton); - services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); - services.AddTransient(); + services.AddTransient(); - // Telegram.Bot's Update class relies on some Newtonsoft attributes, - // so to deserialize it correctly, we need to use this library as well - services.AddControllers().AddNewtonsoftJson(); + // Telegram.Bot's Update class relies on some Newtonsoft attributes, + // so to deserialize it correctly, we need to use this library as well + services.AddControllers().AddNewtonsoftJson(); - services.Configure(Configuration.GetSection("General")); - services.Configure(Configuration.GetSection("Bandwidth")); - } + services.Configure(Configuration.GetSection("General")); + services.Configure(Configuration.GetSection("Bandwidth")); } } From b6f31e0c706eec6c7bd276e55b4cd7539085b1fb Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 25 Apr 2024 16:32:47 +0700 Subject: [PATCH 05/37] db: finally added an "initial" (massive sarcasm quotes here) migration for the sqlite file, no longer rely on a manually prebuilt file (i did questionable things six years ago, time to improve) --- .../20240425092744_InitialCreate.Designer.cs | 37 ++++++++++++++++++ .../20240425092744_InitialCreate.cs | 33 ++++++++++++++++ .../Migrations/RecordContextModelSnapshot.cs | 34 ++++++++++++++++ WebToTelegramCore/WebToTelegramCore.csproj | 7 ++-- WebToTelegramCore/database.sqlite | Bin 20480 -> 0 bytes 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 WebToTelegramCore/Migrations/20240425092744_InitialCreate.Designer.cs create mode 100644 WebToTelegramCore/Migrations/20240425092744_InitialCreate.cs create mode 100644 WebToTelegramCore/Migrations/RecordContextModelSnapshot.cs delete mode 100644 WebToTelegramCore/database.sqlite diff --git a/WebToTelegramCore/Migrations/20240425092744_InitialCreate.Designer.cs b/WebToTelegramCore/Migrations/20240425092744_InitialCreate.Designer.cs new file mode 100644 index 0000000..9c63f59 --- /dev/null +++ b/WebToTelegramCore/Migrations/20240425092744_InitialCreate.Designer.cs @@ -0,0 +1,37 @@ +// +using Bnfour.WebToTelegramCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bnfour.WebToTelegramCore.Migrations +{ + [DbContext(typeof(RecordContext))] + [Migration("20240425092744_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + + modelBuilder.Entity("Bnfour.WebToTelegramCore.Models.Record", b => + { + b.Property("Token") + .HasColumnType("TEXT"); + + b.Property("AccountNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Token"); + + b.ToTable("Records", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WebToTelegramCore/Migrations/20240425092744_InitialCreate.cs b/WebToTelegramCore/Migrations/20240425092744_InitialCreate.cs new file mode 100644 index 0000000..85f2c5c --- /dev/null +++ b/WebToTelegramCore/Migrations/20240425092744_InitialCreate.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bnfour.WebToTelegramCore.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Records", + columns: table => new + { + Token = table.Column(type: "TEXT", nullable: false), + AccountNumber = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Records", x => x.Token); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Records"); + } + } +} diff --git a/WebToTelegramCore/Migrations/RecordContextModelSnapshot.cs b/WebToTelegramCore/Migrations/RecordContextModelSnapshot.cs new file mode 100644 index 0000000..e926928 --- /dev/null +++ b/WebToTelegramCore/Migrations/RecordContextModelSnapshot.cs @@ -0,0 +1,34 @@ +// +using Bnfour.WebToTelegramCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bnfour.WebToTelegramCore.Migrations +{ + [DbContext(typeof(RecordContext))] + partial class RecordContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + + modelBuilder.Entity("Bnfour.WebToTelegramCore.Models.Record", b => + { + b.Property("Token") + .HasColumnType("TEXT"); + + b.Property("AccountNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Token"); + + b.ToTable("Records", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WebToTelegramCore/WebToTelegramCore.csproj b/WebToTelegramCore/WebToTelegramCore.csproj index 2780a80..ca53426 100644 --- a/WebToTelegramCore/WebToTelegramCore.csproj +++ b/WebToTelegramCore/WebToTelegramCore.csproj @@ -11,14 +11,15 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - - PreserveNewest - PreserveNewest diff --git a/WebToTelegramCore/database.sqlite b/WebToTelegramCore/database.sqlite deleted file mode 100644 index 56d529bc4cedbcc05ea17563de9c02658e491029..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI((QDH{9Ki9rWOKHxtst_8G6;Lxz{=`$(>J38Dc0(0O06$xn%<UEUoxM~SjdqIlvb)`7Sqq!iCAO9)Zc zv980^Vd&V@v8E$0GOyflEQ<&7LrEtX6DgFM`37-xK2@<7tI(e)FNnsoN$7>@+*>|r?M2(MUB`0m z=S|1zFLmR-wOcIo-RM+>{etVfcdgbN9i3*=>a-fi9p}Dowu2y=g-L5R8K}5lXtZ4C zrPIF3)G2S3LihHBmx3<1?r5J@n_4#*qJb#u%i*s+O{(1GK($b0X zQTk45`jK|^Z;x!NAc}`mubx%b=Oz6vs#Q5ZTQFyrv%_`Io$Tt){}fB-=6cdEDwmbZ zv`Rq$0R#|0009ILKmY**5I_Kd)etC2bKCHg4**x||36lDaP%Aj1Q0*~ z0R#|0009ILKmY**GJ$*2tQpxRfG1D){XsBPC*pfCZBF<9|Gf5ZKWqpffB*srAb Date: Thu, 25 Apr 2024 16:37:35 +0700 Subject: [PATCH 06/37] minor string change: en dash instead of minus for year interval imagine being somewhat of a typographics nerd --- WebToTelegramCore/Resources/Locale.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebToTelegramCore/Resources/Locale.cs b/WebToTelegramCore/Resources/Locale.cs index 04f90dc..d58a71c 100644 --- a/WebToTelegramCore/Resources/Locale.cs +++ b/WebToTelegramCore/Resources/Locale.cs @@ -20,7 +20,7 @@ public static class Locale **Dotnet Telegram forwarder** v {0}\. [Open\-source\!](https://github.com/bnfour/dotnet-telegram-forwarder) - by bnfour, 2018, 2020\-2024\. + by bnfour, 2018, 2020\–2024\. """; /// From c6e95d2dfe22421cde9e3a716546ffb12ea618d7 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 25 Apr 2024 16:49:33 +0700 Subject: [PATCH 07/37] token generation: replaced exception about alphabet length to a log warning, removing a very-very old TODO comment yay --- .../Services/TokenGeneratorService.cs | 127 +++++++++--------- 1 file changed, 63 insertions(+), 64 deletions(-) diff --git a/WebToTelegramCore/Services/TokenGeneratorService.cs b/WebToTelegramCore/Services/TokenGeneratorService.cs index 7c9219c..145397b 100644 --- a/WebToTelegramCore/Services/TokenGeneratorService.cs +++ b/WebToTelegramCore/Services/TokenGeneratorService.cs @@ -1,86 +1,85 @@ -using System; -using System.Security.Cryptography; +using System.Security.Cryptography; using System.Linq; using System.Text; using Bnfour.WebToTelegramCore.Interfaces; +using Microsoft.Extensions.Logging; -namespace Bnfour.WebToTelegramCore.Services +namespace Bnfour.WebToTelegramCore.Services; + +/// +/// Class that actually generates tokens. +/// +internal class TokenGeneratorService : ITokenGeneratorService { /// - /// Class that actually generates tokens. + /// Hardcoded token length. /// - internal class TokenGeneratorService : ITokenGeneratorService - { - /// - /// Hardcoded token length. - /// - private const int _tokenLength = 16; + private const int _tokenLength = 16; - /// - /// Symbols that token may contain. Total count must be a divider of 256 - /// in order to random to be even. - /// - private static readonly char[] _alphabet = ("0123456789" + "+=" + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz").ToCharArray(); + /// + /// Symbols that token may contain. Total count must be a divider of 256 + /// in order to random to be even. + /// + private static readonly char[] _alphabet = ("0123456789" + "+=" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz").ToCharArray(); - /// - /// DB context. Used to check for collisions with existing tokens. - /// - private readonly RecordContext _context; + /// + /// DB context. Used to check for collisions with existing tokens. + /// + private readonly RecordContext _context; - /// - /// Class constructor. - /// - public TokenGeneratorService(RecordContext context) - { - _context = context; + /// + /// Class constructor. + /// + // the logger is not stored in a field because the only place it's called (yet) is in here + public TokenGeneratorService(RecordContext context, ILogger logger) + { + _context = context; - // sanity check for random evenness - // TODO consider it to be a warning instead of an exception - if (256 % _alphabet.Length != 0) - { - throw new ApplicationException("Selected alphabet does not map evenly " + - "to bytes value. Consider alphabet length that is a divider of 256."); - } + // check for random evenness + if (256 % _alphabet.Length != 0) + { + logger.LogWarning("Selected alphabet does not map evenly " + + "to bytes value. Consider alphabet length that is a divider of 256."); } + } - /// - /// Generates a token and ensures it is not yet assigned to other accounts. - /// - /// An unique token. - public string Generate() + /// + /// Generates a token and ensures it is not yet assigned to other accounts. + /// + /// An unique token. + public string Generate() + { + string token = null; + var done = false; + // let's pretend we're serious business just for a moment + using (var random = RandomNumberGenerator.Create()) { - string token = null; - var done = false; - // let's pretend we're serious business just for a moment - using (var random = RandomNumberGenerator.Create()) + while (!done) { - while (!done) - { - token = GenerateRandom(random); - done = !_context.Records.Any(r => r.Token == token); - } + token = GenerateRandom(random); + done = !_context.Records.Any(r => r.Token == token); } - return token; } + return token; + } - /// - /// Actual token generation method. - /// - /// A _secure_ random generator to use. - /// A completely random token. - private string GenerateRandom(RandomNumberGenerator random) - { - var randomBytes = new byte[_tokenLength]; - random.GetBytes(randomBytes); - - var sb = new StringBuilder(); - foreach (var b in randomBytes) - { - sb.Append(_alphabet[b % _alphabet.Length]); - } + /// + /// Actual token generation method. + /// + /// A _secure_ random generator to use. + /// A completely random token. + private string GenerateRandom(RandomNumberGenerator random) + { + var randomBytes = new byte[_tokenLength]; + random.GetBytes(randomBytes); - return sb.ToString(); + var sb = new StringBuilder(); + foreach (var b in randomBytes) + { + sb.Append(_alphabet[b % _alphabet.Length]); } + + return sb.ToString(); } } From 2e1cf14524baca870afb785dadb209942d32fc59 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Sun, 28 Apr 2024 13:17:44 +0700 Subject: [PATCH 08/37] pass the regeneration delay as Retry-After header on 429 responses --- .../Controllers/WebApiController.cs | 131 ++++++++++-------- 1 file changed, 70 insertions(+), 61 deletions(-) diff --git a/WebToTelegramCore/Controllers/WebApiController.cs b/WebToTelegramCore/Controllers/WebApiController.cs index 60e5216..2638289 100644 --- a/WebToTelegramCore/Controllers/WebApiController.cs +++ b/WebToTelegramCore/Controllers/WebApiController.cs @@ -4,76 +4,85 @@ using Bnfour.WebToTelegramCore.Exceptions; using Bnfour.WebToTelegramCore.Interfaces; using Bnfour.WebToTelegramCore.Models; +using Microsoft.Extensions.Options; +using Bnfour.WebToTelegramCore.Options; -namespace Bnfour.WebToTelegramCore.Controllers +namespace Bnfour.WebToTelegramCore.Controllers; + +public class WebApiController : Controller { - public class WebApiController : Controller + /// + /// Field to store injected web API service. + /// + private readonly IOwnApiService _ownApi; + + /// + /// Stores value as a string + /// to be included as a Retry-After header when returning Too Many Requests response. + /// + private readonly string _retryAfter; + + public WebApiController(IOwnApiService ownApi, IOptions options) { - /// - /// Field to store injected web API service. - /// - private readonly IOwnApiService _ownApi; + _ownApi = ownApi; + _retryAfter = options.Value.SecondsPerRegeneration.ToString(); + } - public WebApiController(IOwnApiService ownApi) + // POST /api + /// + /// Handles web API calls. + /// + /// Request object in POST request body. + /// HTTP status code result indicating whether the request was handled + /// successfully, or one of the error codes. + [HttpPost, Route("api")] + public async Task HandleWebApi([FromBody] Request request) + { + // deny malformed requests as detected by binder, or by "business" logic in here + if (!ModelState.IsValid || !IsValid(request)) { - _ownApi = ownApi; + return BadRequest(); } - - // POST /api - /// - /// Handles web API calls. - /// - /// Request object in POST request body. - /// HTTP status code result indicating whether the request was handled - /// successfully, or one of the error codes. - [HttpPost, Route("api")] - public async Task HandleWebApi([FromBody] Request request) + try { - // deny malformed requests as detected by binder, or by "business" logic in here - if (!ModelState.IsValid || !IsValid(request)) - { - return BadRequest(); - } - try - { - await _ownApi.HandleRequest(request); - return Ok(); - } - catch (TokenNotFoundException) - { - return NotFound(); - } - catch (BandwidthExceededException) - { - return StatusCode((int)HttpStatusCode.TooManyRequests); - } - // if markdown formatting or sticker ID is malformed, relay Telegram's "bad request" to the user - catch (Telegram.Bot.Exceptions.ApiRequestException ex) when (ex.Message.StartsWith("Bad Request")) - { - return BadRequest(); - } - catch - { - return StatusCode((int)HttpStatusCode.InternalServerError); - } + await _ownApi.HandleRequest(request); + return Ok(); } - - // note that malformed markdown or invalid IDs are caught by Telegram backend, - // this merely checks that the fields are set correctly, not the contents - - /// - /// Checks for validity not caught by model binder. - /// Assures that one of either - /// or is set. - /// - /// Request object. - /// Whether the request is valid by its fields. - private bool IsValid(Request request) + catch (TokenNotFoundException) + { + return NotFound(); + } + catch (BandwidthExceededException) + { + Response.Headers.RetryAfter = _retryAfter; + return StatusCode((int)HttpStatusCode.TooManyRequests); + } + // if markdown formatting or sticker ID is malformed, relay Telegram's "bad request" to the user + catch (Telegram.Bot.Exceptions.ApiRequestException ex) when (ex.Message.StartsWith("Bad Request")) + { + return BadRequest(); + } + catch { - var hasMessage = !string.IsNullOrEmpty(request.Message); - var hasSticker = !string.IsNullOrEmpty(request.Sticker); - // either a message or a sticker, not both, not neither - return hasMessage ^ hasSticker; + return StatusCode((int)HttpStatusCode.InternalServerError); } } + + // note that malformed markdown or invalid IDs are caught by Telegram backend, + // this merely checks that the fields are set correctly, not the contents + + /// + /// Checks for validity not caught by model binder. + /// Assures that one of either + /// or is set. + /// + /// Request object. + /// Whether the request is valid by its fields. + private bool IsValid(Request request) + { + var hasMessage = !string.IsNullOrEmpty(request.Message); + var hasSticker = !string.IsNullOrEmpty(request.Sticker); + // either a message or a sticker, not both, not neither + return hasMessage ^ hasSticker; + } } From d1c013ecf85e2df08fd1172c115967b0b1bfb39d Mon Sep 17 00:00:00 2001 From: Bn4 Date: Sun, 28 Apr 2024 13:20:42 +0700 Subject: [PATCH 09/37] readme: documented the Retry-After header usage --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index ec9a821..037b0de 100644 --- a/readme.md +++ b/readme.md @@ -46,7 +46,7 @@ Client should check that the request is well-formed and meets the specifications * `404 Not Found` if supplied token is not present in the database Client should check that the token they provided is valid and has not been removed or changed, and retry with the correct one. * `429 Too Many Requests` if current limit of sent messages is exhausted -Client should retry later, after waiting at least one minute (on default throughput config). +Client should retry later, the included `Retry-After` header suggests the amount of seconds to wait before trying again. * `500 Internal Server Error` in case anything goes wrong Client can try to retry later, but ¯\\\_(ツ)\_/¯ From f092609b6d3e9ecee125fea42334ef3aad24a49c Mon Sep 17 00:00:00 2001 From: Bn4 Date: Sun, 28 Apr 2024 13:57:47 +0700 Subject: [PATCH 10/37] minor overdue fixes --- WebToTelegramCore/BotCommands/CreateCommand.cs | 2 +- WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/WebToTelegramCore/BotCommands/CreateCommand.cs b/WebToTelegramCore/BotCommands/CreateCommand.cs index 0f84f19..a49665d 100644 --- a/WebToTelegramCore/BotCommands/CreateCommand.cs +++ b/WebToTelegramCore/BotCommands/CreateCommand.cs @@ -65,7 +65,7 @@ public override string Process(Record record) /// /// Actual method that does registration or denies it. /// - /// Record to process. Is null if working properly. + /// Record to process. Does not contain a token yet, only user id. /// Message with new token or message stating that registration /// is closed for good. private string InternalProcess(Record record) diff --git a/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs b/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs index 7fc8815..67a4a9b 100644 --- a/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs +++ b/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs @@ -13,7 +13,6 @@ public abstract class GuestOnlyCommandBase : BotCommandBase, IBotCommand /// /// Constructor that sets up error message. /// - /// Locale options to use. public GuestOnlyCommandBase() : base() { } /// From 207ae9f0a2885f19bd058037db44b6c0d3043e1a Mon Sep 17 00:00:00 2001 From: Bn4 Date: Sun, 28 Apr 2024 14:59:10 +0700 Subject: [PATCH 11/37] database changes, preparation: removed RecordService, changed Record.UsageCounter logic - UsageCounter now starts at 0 and goes up on messages sent; down with time -- _should_ work better on bandwidth options change after everything is stored in the DB (spoilers!) - RecordService former logic now split between context (updates the counter based on time elapsed) and sending service (does rate-limiting) actual logic is in the Record class itself --- .../BotCommands/ConfirmCommand.cs | 158 +++++----- .../BotCommands/CreateCommand.cs | 128 ++++---- WebToTelegramCore/BotCommands/TokenCommand.cs | 117 ++++---- .../Interfaces/IRecordService.cs | 28 -- WebToTelegramCore/Models/Record.cs | 84 ++++-- WebToTelegramCore/Options/BandwidthOptions.cs | 30 +- WebToTelegramCore/RecordContext.cs | 113 +++---- WebToTelegramCore/Services/OwnApiService.cs | 95 +++--- WebToTelegramCore/Services/RecordService.cs | 78 ----- .../Services/TelegramApiService.cs | 280 +++++++++--------- WebToTelegramCore/Startup.cs | 10 +- 11 files changed, 511 insertions(+), 610 deletions(-) delete mode 100644 WebToTelegramCore/Interfaces/IRecordService.cs delete mode 100644 WebToTelegramCore/Services/RecordService.cs diff --git a/WebToTelegramCore/BotCommands/ConfirmCommand.cs b/WebToTelegramCore/BotCommands/ConfirmCommand.cs index d422c57..2d3d8b6 100644 --- a/WebToTelegramCore/BotCommands/ConfirmCommand.cs +++ b/WebToTelegramCore/BotCommands/ConfirmCommand.cs @@ -1,106 +1,94 @@ -using System; -using Bnfour.WebToTelegramCore.Data; +using Bnfour.WebToTelegramCore.Data; using Bnfour.WebToTelegramCore.Interfaces; using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Class that implements /confirm command which either deletes user's token or +/// replaces it with a new one after a request via previous command. +/// +public class ConfirmCommand : ConfirmationCommandBase, IBotCommand { /// - /// Class that implements /confirm command which either deletes user's token or - /// replaces it with a new one after a request via previous command. + /// Command's text. /// - public class ConfirmCommand : ConfirmationCommandBase, IBotCommand - { - /// - /// Command's text. - /// - public override string Command => "/confirm"; + public override string Command => "/confirm"; - /// - /// Database context reference to perform DB operations. - /// - private readonly RecordContext _context; + /// + /// Database context reference to perform DB operations. + /// + private readonly RecordContext _context; - /// - /// Token generator service reference. - /// - private readonly ITokenGeneratorService _tokenGenerator; + /// + /// Token generator service reference. + /// + private readonly ITokenGeneratorService _tokenGenerator; - /// - /// Record manipulation service helper reference. - /// - private readonly IRecordService _recordService; + /// + /// Constructor. + /// + /// Database context to use. + /// Token generator to use. + /// Record helper to use. + public ConfirmCommand(RecordContext context, ITokenGeneratorService generator) : base() + { + _context = context; + _tokenGenerator = generator; + } - /// - /// Constructor. - /// - /// Database context to use. - /// Token generator to use. - /// Record helper to use. - public ConfirmCommand(RecordContext context, ITokenGeneratorService generator, - IRecordService recordService) : base() + /// + /// Method to carry on confirmed destructive operations. + /// + /// Record associated with user who sent the command. + /// End-user readable result of the operation. + public override string Process(Record record) + { + string baseResult = base.Process(record); + if (baseResult != null) { - _context = context; - _tokenGenerator = generator; - _recordService = recordService; + return baseResult; } - /// - /// Method to carry on confirmed destructive operations. - /// - /// Record associated with user who sent the command. - /// End-user readable result of the operation. - public override string Process(Record record) + if (record.State == RecordState.PendingDeletion) { - string baseResult = base.Process(record); - if (baseResult != null) - { - return baseResult; - } - - if (record.State == RecordState.PendingDeletion) - { - return Delete(record); - } - else - { - return Regenerate(record); - } + return Delete(record); } - - /// - /// Regenerates user's token. - /// - /// Record to generate new token for. - /// Message with new token. - private string Regenerate(Record record) + else { - string newToken = _tokenGenerator.Generate(); - // so apparently, primary key cannot be changed, - // create a new record and transfer all data but token and state (it's pending regeneration right now) - var newRecord = _recordService.Create(newToken, record.AccountNumber); - // consider moving these to Create params with default values? - newRecord.UsageCounter = record.UsageCounter; - newRecord.LastSuccessTimestamp = record.LastSuccessTimestamp; - - _context.Remove(record); - _context.Add(newRecord); - _context.SaveChanges(); - - return String.Format(Locale.ConfirmRegeneration, newToken); + return Regenerate(record); } + } - /// - /// Deletes specified record from the database. - /// - /// Record to remove. - /// Message about performed operation. - private string Delete(Record record) - { - _context.Remove(record); - _context.SaveChanges(); - return Locale.ConfirmDeletion; - } + /// + /// Regenerates user's token. + /// + /// Record to generate new token for. + /// Message with new token. + private string Regenerate(Record record) + { + string newToken = _tokenGenerator.Generate(); + // so apparently, primary key cannot be changed, + // create a new record and transfer all data but token and state (it's pending regeneration right now) + var newRecord = new Record(newToken, record.AccountNumber, record.UsageCounter, record.LastUpdate); + + _context.Remove(record); + _context.Add(newRecord); + _context.SaveChanges(); + + return string.Format(Locale.ConfirmRegeneration, newToken); + } + + /// + /// Deletes specified record from the database. + /// + /// Record to remove. + /// Message about performed operation. + private string Delete(Record record) + { + _context.Remove(record); + _context.SaveChanges(); + return Locale.ConfirmDeletion; } } diff --git a/WebToTelegramCore/BotCommands/CreateCommand.cs b/WebToTelegramCore/BotCommands/CreateCommand.cs index a49665d..d2488ac 100644 --- a/WebToTelegramCore/BotCommands/CreateCommand.cs +++ b/WebToTelegramCore/BotCommands/CreateCommand.cs @@ -1,87 +1,79 @@ -using System; -using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Interfaces; using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// /create command, allows user to create a token and start using the bot. +/// +public class CreateCommand : GuestOnlyCommandBase, IBotCommand { /// - /// /create command, allows user to create a token and start using the bot. + /// Command's text. /// - public class CreateCommand : GuestOnlyCommandBase, IBotCommand - { - /// - /// Command's text. - /// - public override string Command => "/create"; + public override string Command => "/create"; - /// - /// Field to store database context reference. - /// - private readonly RecordContext _context; + /// + /// Field to store database context reference. + /// + private readonly RecordContext _context; - /// - /// Field to store token generator reference. - /// - private readonly ITokenGeneratorService _generator; + /// + /// Field to store token generator reference. + /// + private readonly ITokenGeneratorService _generator; - /// - /// Record manipulation service helper reference. - /// - private readonly IRecordService _recordService; + /// + /// Field to store whether registration is enabled. True is enabled. + /// + private readonly bool _isRegistrationEnabled; - /// - /// Field to store whether registration is enabled. True is enabled. - /// - private readonly bool _isRegistrationEnabled; + /// + /// Constructor that injects dependencies and sets up registration state. + /// + /// Database context to use. + /// Token generator service to use. + /// State of registration. + public CreateCommand(RecordContext context, + ITokenGeneratorService generator, + bool isRegistrationEnabled) : base() + { + _context = context; + _generator = generator; - /// - /// Constructor that injects dependencies and sets up registration state. - /// - /// Database context to use. - /// Token generator service to use. - /// Record helper service to use. - /// State of registration. - public CreateCommand(RecordContext context, ITokenGeneratorService generator, - IRecordService recordService, bool isRegistrationEnabled) : base() - { - _context = context; - _generator = generator; - _recordService = recordService; + _isRegistrationEnabled = isRegistrationEnabled; + } - _isRegistrationEnabled = isRegistrationEnabled; - } + /// + /// Method to process the command. + /// + /// Record to process. + /// Message with new token or error when there is one already. + public override string Process(Record record) + { + return base.Process(record) ?? InternalProcess(record); + } - /// - /// Method to process the command. - /// - /// Record to process. - /// Message with new token or error when there is one already. - public override string Process(Record record) + /// + /// Actual method that does registration or denies it. + /// + /// Record to process. Does not contain a token yet, only user id. + /// Message with new token or message stating that registration + /// is closed for good. + private string InternalProcess(Record record) + { + if (_isRegistrationEnabled) { - return base.Process(record) ?? InternalProcess(record); + string token = _generator.Generate(); + var r = new Record(token, record.AccountNumber); + _context.Add(r); + _context.SaveChanges(); + return string.Format(Locale.CreateSuccess, token); } - - /// - /// Actual method that does registration or denies it. - /// - /// Record to process. Does not contain a token yet, only user id. - /// Message with new token or message stating that registration - /// is closed for good. - private string InternalProcess(Record record) + else { - if (_isRegistrationEnabled) - { - string token = _generator.Generate(); - var r = _recordService.Create(token, record.AccountNumber); - _context.Add(r); - _context.SaveChanges(); - return String.Format(Locale.CreateSuccess, token); - } - else - { - return Locale.CreateGoAway; - } + return Locale.CreateGoAway; } } } diff --git a/WebToTelegramCore/BotCommands/TokenCommand.cs b/WebToTelegramCore/BotCommands/TokenCommand.cs index d921654..39a8d76 100644 --- a/WebToTelegramCore/BotCommands/TokenCommand.cs +++ b/WebToTelegramCore/BotCommands/TokenCommand.cs @@ -4,24 +4,24 @@ using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// /token command. Displays user's token and usage hint. +/// +public class TokenCommand : UserOnlyCommandBase, IBotCommand { /// - /// /token command. Displays user's token and usage hint. + /// Command's text. /// - public class TokenCommand : UserOnlyCommandBase, IBotCommand - { - /// - /// Command's text. - /// - public override string Command => "/token"; + public override string Command => "/token"; - /// - /// Random quotes to display as message example. - /// - private readonly string[] _textExamples = new[] - { - "Hello world!", + /// + /// Random quotes to display as message example. + /// + private readonly string[] _textExamples = + [ + "Hello world!", "Timeline lost", "send help", "inhale", @@ -41,57 +41,56 @@ public class TokenCommand : UserOnlyCommandBase, IBotCommand "Everything in my life can unironically be solved with a full combo", "Synchronization failed", "Friends are so nice! heart" - }; + ]; - /// - /// Random sticker IDs to display as message example. - /// - private readonly string[] _stickerExamples = new[] - { - "CAACAgEAAxkBAAIBOGRXiFSUVC7ZsS_5q2TGB1-fpJhTAAIsEQACmX-IAgyEv_-IJmTBLwQ", + /// + /// Random sticker IDs to display as message example. + /// + private readonly string[] _stickerExamples = + [ + "CAACAgEAAxkBAAIBOGRXiFSUVC7ZsS_5q2TGB1-fpJhTAAIsEQACmX-IAgyEv_-IJmTBLwQ", "CAACAgQAAxkBAAIBOmRXiFra2cpvwzCWA0QYoVkmSwTlAAKiAgACZsMpDOGLaU-TMrXqLwQ", "CAACAgQAAxkBAAIBPGRXiGKI9bvYdPYccZJ2b0JZ-MrJAALLAANGp6MbBxkKM3Lb7egvBA", - }; + ]; - /// - /// Field to store API endpoint address. - /// - private readonly string _apiEndpoint; + /// + /// Field to store API endpoint address. + /// + private readonly string _apiEndpoint; + + /// + /// Constructor. + /// + /// API endpoint URL. + public TokenCommand(string apiEndpoint) : base() + { + _apiEndpoint = apiEndpoint; + } - /// - /// Constructor. - /// - /// API endpoint URL. - public TokenCommand(string apiEndpoint) : base() - { - _apiEndpoint = apiEndpoint; - } + /// + /// Method to process command. + /// + /// Record associated with user who sent the command. + /// Message with token and API usage, or error message if user + /// has no token. + public override string Process(Record record) + { + return base.Process(record) ?? InternalProcess(record); + } - /// - /// Method to process command. - /// - /// Record associated with user who sent the command. - /// Message with token and API usage, or error message if user - /// has no token. - public override string Process(Record record) - { - return base.Process(record) ?? InternalProcess(record); - } + /// + /// Actual method to create returned message with placeholders filled. + /// + /// Record to process. + /// Message with token and API usage example. + private string InternalProcess(Record record) + { + var random = new Random(); + var text = _textExamples[random.Next(0, _textExamples.Length)]; + var sticker = _stickerExamples[random.Next(0, _stickerExamples.Length)]; - /// - /// Actual method to create returned message with placeholders filled. - /// - /// Record to process. - /// Message with token and API usage example. - private string InternalProcess(Record record) - { - var random = new Random(); - var text = _textExamples[random.Next(0, _textExamples.Length)]; - var sticker = _stickerExamples[random.Next(0, _stickerExamples.Length)]; - - return String.Format(Locale.TokenTemplate, TelegramMarkdownFormatter.Escape(record.Token), - TelegramMarkdownFormatter.Escape(_apiEndpoint + "/api"), TelegramMarkdownFormatter.Escape(text), - TelegramMarkdownFormatter.Escape(sticker)); - } + return string.Format(Locale.TokenTemplate, TelegramMarkdownFormatter.Escape(record.Token), + TelegramMarkdownFormatter.Escape(_apiEndpoint + "/api"), TelegramMarkdownFormatter.Escape(text), + TelegramMarkdownFormatter.Escape(sticker)); } } diff --git a/WebToTelegramCore/Interfaces/IRecordService.cs b/WebToTelegramCore/Interfaces/IRecordService.cs deleted file mode 100644 index 73afb89..0000000 --- a/WebToTelegramCore/Interfaces/IRecordService.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Bnfour.WebToTelegramCore.Models; - -namespace Bnfour.WebToTelegramCore.Interfaces -{ - /// - /// Interface that makes managing records look easy for its users. - /// - public interface IRecordService - { - /// - /// Creates a new Record, setting common default values - /// for all instances. - /// - /// Token to create Record with. - /// Account id to create token with. - /// Record with all properties populated. - Record Create(string token, long accountId); - - /// - /// Checks if Record holds enough charges to be able to send a message - /// immediately ( > 0). If so, returns true, - /// and updates Record's state to indicate the message was sent just now. - /// - /// Record to check and possibly update. - /// True if message can and should be sent, false otherwise. - bool CheckIfCanSend(Record record); - } -} diff --git a/WebToTelegramCore/Models/Record.cs b/WebToTelegramCore/Models/Record.cs index 8a45859..1e19d1e 100644 --- a/WebToTelegramCore/Models/Record.cs +++ b/WebToTelegramCore/Models/Record.cs @@ -1,39 +1,63 @@ using System; using Bnfour.WebToTelegramCore.Data; -namespace Bnfour.WebToTelegramCore.Models +namespace Bnfour.WebToTelegramCore.Models; + +/// +/// Internal representation of Token - ID relationship with additional +/// fields to control usage. +/// +public class Record(string token, long accountNumber, int usageCounter = 0, DateTime? lastUpdated = null) { /// - /// Internal representation of Token - ID relationship with additional - /// fields to control usage. + /// Auth token associated with this record. Primary key in the DB. + /// + public string Token { get; private set; } = token; + + /// + /// ID of connected Telegram account. + /// + public long AccountNumber { get; private set; } = accountNumber; + + /// + /// Holds amount of messages sent recently. + /// + public int UsageCounter { get; private set; } = usageCounter; + + /// + /// Timestamp of last successful request. Used to update UsageCounter. + /// + public DateTime LastUpdate { get; private set; } = lastUpdated ?? DateTime.UtcNow; + + /// + /// Field to store whether this Record awaits user confirmation + /// on destructive command. + /// + public RecordState State { get; set; } = RecordState.Normal; + + /// + /// Recalculates UsageCounter after re-materialization. + /// + /// Amount of time it takes to subtract one from UsageCounter. + public void Update(TimeSpan regenerationTime) + { + var sinceLastUpdate = DateTime.UtcNow - LastUpdate; + var regenerated = (int)(sinceLastUpdate / regenerationTime); + // do not update the timestamp if usage counter was not updated + // otherwise, the cooldown time is effectively extended for another regenerationTime + if (regenerated > 0) + { + UsageCounter = Math.Max(0, UsageCounter - regenerated); + LastUpdate = DateTime.UtcNow; + } + } + + /// + /// Updates the record on successful sending of a message. /// - public class Record + public void MarkAsUsed() { - /// - /// Auth token associated with this record. Primary key in the DB. - /// - public string Token { get; set; } - - /// - /// ID of connected Telegram account. - /// - public long AccountNumber { get; set; } - - /// - /// Holds amount of messages available immidiately. - /// - public int UsageCounter { get; set; } - - /// - /// Timestamp of last successful request. Used to calculate how much to add - /// to UsageCounter. - /// - public DateTime LastSuccessTimestamp { get; set; } - - /// - /// Field to store whether this Record awaits user confirmation - /// on destructive command. - /// - public RecordState State { get; set; } + UsageCounter++; + LastUpdate = DateTime.UtcNow; } } diff --git a/WebToTelegramCore/Options/BandwidthOptions.cs b/WebToTelegramCore/Options/BandwidthOptions.cs index dea1812..fd523f3 100644 --- a/WebToTelegramCore/Options/BandwidthOptions.cs +++ b/WebToTelegramCore/Options/BandwidthOptions.cs @@ -1,20 +1,20 @@ -namespace Bnfour.WebToTelegramCore.Options +namespace Bnfour.WebToTelegramCore.Options; + +/// +/// Class that represents app's setting related to message +/// throughput limitations. +/// +public class BandwidthOptions { /// - /// Class that represents app's setting related to message - /// throughput limitations. + /// Represents initial number of messages available. /// - public class BandwidthOptions - { - /// - /// Represents initial number of messages available. - /// - public int InitialCount { get; set; } + // TODO the logic changed, rename this + public int InitialCount { get; set; } - /// - /// Represents amount of seconds after last message send to regenerate - /// one message. - /// - public int SecondsPerRegeneration { get; set; } - } + /// + /// Represents amount of seconds after last message send to regenerate + /// one message. + /// + public int SecondsPerRegeneration { get; set; } } diff --git a/WebToTelegramCore/RecordContext.cs b/WebToTelegramCore/RecordContext.cs index 46ce64d..c0a2356 100644 --- a/WebToTelegramCore/RecordContext.cs +++ b/WebToTelegramCore/RecordContext.cs @@ -1,60 +1,73 @@ using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; using Bnfour.WebToTelegramCore.Models; +using System; +using Microsoft.Extensions.Options; +using Bnfour.WebToTelegramCore.Options; -namespace Bnfour.WebToTelegramCore +namespace Bnfour.WebToTelegramCore; + +/// +/// Class that encapsulates Records for EF usage. +/// +public class RecordContext : DbContext { /// - /// Class that encapsulates Records for EF usage. + /// Record collection for manipulation within the app. + /// + public DbSet Records { get; private set; } + + private readonly TimeSpan _regenerationTime; + + /// + /// Constructor that does literally nothing + /// but is required anyway due to base call. + /// + /// Options for the context. + public RecordContext(DbContextOptions contextOptions, + IOptions bandwidthOptions) : base(contextOptions) + { + _regenerationTime = TimeSpan.FromSeconds(bandwidthOptions.Value.SecondsPerRegeneration); + } + + /// + /// Sets model parameters: makes UsageCounter, LastSuccessTimestamp, and State + /// as .NET-only properties, marks token as a primary key. Also explicitly + /// sets table name to match actual DB file. + /// + /// ModelBuilder to use. + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .ToTable("Records") + .Ignore(r => r.UsageCounter) + .Ignore(r => r.LastUpdate) + .Ignore(r => r.State) + .HasKey(r => r.Token); + } + + /// + /// Tries to fetch record by token from the database. + /// Updates usage details based on time elapsed since last update. + /// + /// Token to search for in the DB. + /// Associated Record (with field updated) or null if none found. + public async Task GetRecordByToken(string token) + { + var record = await Records.SingleOrDefaultAsync(r => r.Token.Equals(token)); + record?.Update(_regenerationTime); + + return record; + } + + /// + /// Gets Record associated with a given Telegram ID from the database. /// - public class RecordContext : DbContext + /// ID to search for. + /// Associated Record or null if none present. + public async Task GetRecordByAccountId(long accountId) { - /// - /// Record collection for manipulation within the app. - /// - public DbSet Records { get; private set; } - - /// - /// Constructor that does literally nothing - /// but is required anyway due to base call. - /// - /// Options for the context. - public RecordContext(DbContextOptions options) : base(options) { } - - /// - /// Sets model parameters: makes UsageCounter, LastSuccessTimestamp, and State - /// as .NET-only properties, marks token as a primary key. Also explicitly - /// sets table name to match actual DB file. - /// - /// ModelBuilder to use. - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity() - .ToTable("Records") - .Ignore(r => r.UsageCounter) - .Ignore(r => r.LastSuccessTimestamp) - .Ignore(r => r.State) - .HasKey(r => r.Token); - } - - /// - /// Tries to fetch record by token from the database. - /// - /// Token to search for in the DB. - /// Associated Record or null if none found. - public async Task GetRecordByToken(string token) - { - return await Records.SingleOrDefaultAsync(r => r.Token.Equals(token)); - } - - /// - /// Gets Record associated with a given Telegram ID from the database. - /// - /// ID to search for. - /// Associated Record or null if none present. - public async Task GetRecordByAccountId(long accountId) - { - return await Records.SingleOrDefaultAsync(r => r.AccountNumber == accountId); - } + // it's not necessary to update the record here, as it won't be used for rate limiting + return await Records.SingleOrDefaultAsync(r => r.AccountNumber == accountId); } } diff --git a/WebToTelegramCore/Services/OwnApiService.cs b/WebToTelegramCore/Services/OwnApiService.cs index c337510..d0a828f 100644 --- a/WebToTelegramCore/Services/OwnApiService.cs +++ b/WebToTelegramCore/Services/OwnApiService.cs @@ -2,66 +2,69 @@ using Bnfour.WebToTelegramCore.Exceptions; using Bnfour.WebToTelegramCore.Interfaces; using Bnfour.WebToTelegramCore.Models; +using Bnfour.WebToTelegramCore.Options; +using Microsoft.Extensions.Options; -namespace Bnfour.WebToTelegramCore.Services +namespace Bnfour.WebToTelegramCore.Services; + +/// +/// Class that handles web API calls. +/// +public class OwnApiService : IOwnApiService { /// - /// Class that handles web API calls. + /// Field to store app's database context. /// - public class OwnApiService : IOwnApiService - { - /// - /// Field to store app's database context. - /// - private readonly RecordContext _context; + private readonly RecordContext _context; - /// - /// Field to store bot service used to send messages. - /// - private readonly ITelegramMessageService _bot; + /// + /// Field to store bot service used to send messages. + /// + private readonly ITelegramMessageService _bot; - /// - /// Field to store Record management service. - /// - private readonly IRecordService _recordService; + /// + /// Field to store maximum amount of messages for rate-limiting. + /// + private readonly int _maxAllowed; - /// - /// Constuctor that injects dependencies. - /// - /// Database context to use. - /// Bot service to use. - /// Bandwidth options. - public OwnApiService(RecordContext context, ITelegramMessageService bot, IRecordService recordService) - { - _context = context; - _bot = bot; - _recordService = recordService; - } + /// + /// Constuctor that injects dependencies. + /// + /// Database context to use. + /// Bot service to use. + /// Bandwidth options. + public OwnApiService(RecordContext context, ITelegramMessageService bot, IOptions options) + { + _context = context; + _bot = bot; + _maxAllowed = options.Value.InitialCount; + } - /// - /// Public method to handle incoming requests. Call underlying internal method. - /// - /// Request to handle. - public async Task HandleRequest(Request request) + /// + /// Public method to handle incoming requests. Call underlying internal method. + /// + /// Request to handle. + public async Task HandleRequest(Request request) + { + var record = await _context.GetRecordByToken(request.Token) ?? throw new TokenNotFoundException(); + if (record.UsageCounter < _maxAllowed) { - var record = await _context.GetRecordByToken(request.Token) ?? throw new TokenNotFoundException(); - if (_recordService.CheckIfCanSend(record)) + // request with both message and sticker should be ignored earlier, + // but prefer text over sticker just in case + if (!string.IsNullOrEmpty(request.Message)) { - // request with both message and sticker should be ignored earlier, - // but prefer text over sticker just in case - if (!string.IsNullOrEmpty(request.Message)) - { - await _bot.Send(record.AccountNumber, request.Message, request.Silent, request.Type); - } - else - { - await _bot.SendSticker(record.AccountNumber, request.Sticker, request.Silent); - } + await _bot.Send(record.AccountNumber, request.Message, request.Silent, request.Type); } else { - throw new BandwidthExceededException(); + await _bot.SendSticker(record.AccountNumber, request.Sticker, request.Silent); } + + record.MarkAsUsed(); + } + else + { + throw new BandwidthExceededException(); } } } diff --git a/WebToTelegramCore/Services/RecordService.cs b/WebToTelegramCore/Services/RecordService.cs deleted file mode 100644 index 060131d..0000000 --- a/WebToTelegramCore/Services/RecordService.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.Extensions.Options; -using System; -using Bnfour.WebToTelegramCore.Data; -using Bnfour.WebToTelegramCore.Interfaces; -using Bnfour.WebToTelegramCore.Models; -using Bnfour.WebToTelegramCore.Options; - -namespace Bnfour.WebToTelegramCore.Services -{ - /// - /// Class that makes managing Records look easy for its users. - /// hide_the_pain_harold.jpg - /// - public class RecordService : IRecordService - { - /// - /// Maximum possible amount of messages available immidiately. - /// - private readonly int _counterMax; - - /// - /// Amount of seconds lince last successful API call to regenerate counter. - /// - private readonly TimeSpan _timeToRegen; - - /// - /// Constructor that gets message bandwiths settings for later use. - /// - /// Options to use. - public RecordService(IOptions options) - { - _counterMax = options.Value.InitialCount; - _timeToRegen = TimeSpan.FromSeconds(options.Value.SecondsPerRegeneration); - } - - /// - /// Checks if Record holds enough charges to be able to send a message - /// immediately ( > 0). If so, returns true, - /// and updates Record's state to indicate the message was sent just now. - /// - /// Record to check and possibly update. - /// True if message can and should be sent, false otherwise. - public bool CheckIfCanSend(Record record) - { - var sinceLastSuccess = DateTime.UtcNow - record.LastSuccessTimestamp; - var toAdd = (int)(sinceLastSuccess / _timeToRegen); - - record.UsageCounter = Math.Min(_counterMax, record.UsageCounter + toAdd); - - if (record.UsageCounter > 0) - { - record.LastSuccessTimestamp = DateTime.UtcNow; - record.UsageCounter--; - return true; - } - return false; - } - - /// - /// Creates a new Record, setting common default values - /// for all instances. - /// - /// Token to create Record with. - /// Account id to create token with. - /// Record with all properties populated. - public Record Create(string token, long accountId) - { - return new Record - { - AccountNumber = accountId, - LastSuccessTimestamp = DateTime.UtcNow, - State = RecordState.Normal, - Token = token, - UsageCounter = _counterMax - }; - } - } -} diff --git a/WebToTelegramCore/Services/TelegramApiService.cs b/WebToTelegramCore/Services/TelegramApiService.cs index b36155f..a4e9614 100644 --- a/WebToTelegramCore/Services/TelegramApiService.cs +++ b/WebToTelegramCore/Services/TelegramApiService.cs @@ -12,181 +12,175 @@ using Bnfour.WebToTelegramCore.Options; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.Services +namespace Bnfour.WebToTelegramCore.Services; + +/// +/// Class that implements handling webhook updates. +/// +public class TelegramApiService : ITelegramApiService { /// - /// Class that implements handling webhook updates. + /// Bot's token. Used to verify update origin. + /// + private readonly string _token; + + /// + /// Database context to use. + /// + private readonly RecordContext _context; + + /// + /// Bot service to send messages (and sometimes stickers). + /// + private readonly ITelegramMessageService _bot; + + /// + /// Reference to token generator service. + /// + private readonly ITokenGeneratorService _generator; + + /// + /// List of commands available to the bot. /// - public class TelegramApiService : ITelegramApiService + private readonly List _commands; + + /// + /// Constructor that injects dependencies and configures list of commands. + /// + /// Options that include token. + /// Database context to use. + /// Bot service instance to use. + /// Token generator service to use. + /// Record helper service to use. + public TelegramApiService(IOptions options, + RecordContext context, ITelegramMessageService bot, + ITokenGeneratorService generator) { - /// - /// Bot's token. Used to verify update origin. - /// - private readonly string _token; - - /// - /// Database context to use. - /// - private readonly RecordContext _context; - - /// - /// Bot service to send messages (and sometimes stickers). - /// - private readonly ITelegramMessageService _bot; - - /// - /// Reference to token generator service. - /// - private readonly ITokenGeneratorService _generator; - - /// - /// Record manipulation service helper reference. - /// - private readonly IRecordService _recordService; - - /// - /// List of commands available to the bot. - /// - private readonly List _commands; - - /// - /// Constructor that injects dependencies and configures list of commands. - /// - /// Options that include token. - /// Database context to use. - /// Bot service instance to use. - /// Token generator service to use. - /// Record helper service to use. - public TelegramApiService(IOptions options, - RecordContext context, ITelegramMessageService bot, - ITokenGeneratorService generator, IRecordService recordService) - { - _token = options.Value.Token; - _context = context; - _bot = bot; - _generator = generator; - _recordService = recordService; + _token = options.Value.Token; + _context = context; + _bot = bot; + _generator = generator; - var isRegistrationOpen = options.Value.RegistrationEnabled; + var isRegistrationOpen = options.Value.RegistrationEnabled; - _commands = new List() + _commands = new List() { new StartCommand(isRegistrationOpen), new TokenCommand(options.Value.ApiEndpointUrl), new RegenerateCommand(), new DeleteCommand(isRegistrationOpen), - new ConfirmCommand(_context, _generator, _recordService), + new ConfirmCommand(_context, _generator), new CancelCommand(), new HelpCommand(), new DirectiveCommand(), new AboutCommand(), - new CreateCommand(_context, _generator, _recordService, isRegistrationOpen) + new CreateCommand(_context, _generator, isRegistrationOpen) }; - } + } - /// - /// Method to handle incoming updates from the webhook. - /// - /// Received update. - public async Task HandleUpdate(Update update) + /// + /// Method to handle incoming updates from the webhook. + /// + /// Received update. + public async Task HandleUpdate(Update update) + { + // only handles either text messages, hopefully commands, or stickers + switch (update.Message.Type) { - // only handles either text messages, hopefully commands, or stickers - switch (update.Message.Type) - { - case MessageType.Text: - await HandleTextMessage(update); + case MessageType.Text: + await HandleTextMessage(update); break; - case MessageType.Sticker: - await HandleSticker(update); + case MessageType.Sticker: + await HandleSticker(update); + break; + default: + // TODO do something break; - default: - return; - } } + } + + /// + /// Checks whether passed string is an actual bot token to verify the request + /// actully comes from Telegram backend. + /// + /// Token received from request. + /// True if calledToken is actual token, false otherwise. + public bool IsToken(string calledToken) + { + return calledToken.Equals(_token); + } - /// - /// Checks whether passed string is an actual bot token to verify the request - /// actully comes from Telegram backend. - /// - /// Token received from request. - /// True if calledToken is actual token, false otherwise. - public bool IsToken(string calledToken) + /// + /// Method to handle incoming text updates from the webhook. + /// + /// Received update. + private async Task HandleTextMessage(Update update) + { + long? userId = update?.Message?.From?.Id; + string text = update?.Message?.Text; + // check if update contains everything we need to process it + if (userId == null || string.IsNullOrEmpty(text)) { - return calledToken.Equals(_token); + return; } + // if user has no record associated, make him a mock one with just an account number, + // so we know who they are in case we're going to create them a proper one + Record record = await _context.GetRecordByAccountId(userId.Value) + ?? new Record(null, userId.Value); - /// - /// Method to handle incoming text updates from the webhook. - /// - /// Received update. - private async Task HandleTextMessage(Update update) + IBotCommand handler = null; + string commandText = text.Split(' ').FirstOrDefault(); + // will crash if multiple command classes share same text, who cares + handler = _commands.SingleOrDefault(c => c.Command.Equals(commandText)); + + if (handler != null) { - long? userId = update?.Message?.From?.Id; - string text = update?.Message?.Text; - // check if update contains everything we need to process it - if (userId == null || string.IsNullOrEmpty(text)) - { - return; - } - // if user has no record associated, make him a mock one with just an account number, - // so we know who they are in case we're going to create them a proper one - Record record = await _context.GetRecordByAccountId(userId.Value) - ?? _recordService.Create(null, userId.Value); - - IBotCommand handler = null; - string commandText = text.Split(' ').FirstOrDefault(); - // will crash if multiple command classes share same text, who cares - handler = _commands.SingleOrDefault(c => c.Command.Equals(commandText)); - - if (handler != null) - { - await _bot.Send(userId.Value, handler.Process(record)); - } - else - { - await HandleUnknownText(userId.Value, commandText); - } + await _bot.Send(userId.Value, handler.Process(record)); + } + else + { + await HandleUnknownText(userId.Value, commandText); } + } - /// - /// Handles unknown text sent to bot. - /// 5% chance of cat sticker, regular text otherwise. - /// - /// User to reply to. - /// Received message that was not processed - /// by actual commands. - private async Task HandleUnknownText(long accountId, string text) + /// + /// Handles unknown text sent to bot. + /// 5% chance of cat sticker, regular text otherwise. + /// + /// User to reply to. + /// Received message that was not processed + /// by actual commands. + private async Task HandleUnknownText(long accountId, string text) + { + // suddenly, cat! + if (new Random().Next(0, 19) == 0) { - // suddenly, cat! - if (new Random().Next(0, 19) == 0) - { - await _bot.SendTheSticker(accountId); - } - else - { - string reply = text.StartsWith("/") - ? Locale.ErrorDave - : Locale.ErrorWhat; - await _bot.Send(accountId, reply); - } + await _bot.SendTheSticker(accountId); } + else + { + string reply = text.StartsWith("/") + ? Locale.ErrorDave + : Locale.ErrorWhat; + await _bot.Send(accountId, reply); + } + } - /// - /// Method to handle incoming sticker updates from the webhook. - /// Replies with an ID to be used with the web API. - /// - /// Received update. - private async Task HandleSticker(Update update) + /// + /// Method to handle incoming sticker updates from the webhook. + /// Replies with an ID to be used with the web API. + /// + /// Received update. + private async Task HandleSticker(Update update) + { + long? userId = update?.Message?.From?.Id; + string fileId = update?.Message?.Sticker?.FileId; + // check if update contains everything we need to process it + if (userId == null || string.IsNullOrEmpty(fileId)) { - long? userId = update?.Message?.From?.Id; - string fileId = update?.Message?.Sticker?.FileId; - // check if update contains everything we need to process it - if (userId == null || string.IsNullOrEmpty(fileId)) - { - return; - } - var message = string.Format(Locale.StickerId, fileId); - await _bot.Send(userId.Value, message); + return; } + var message = string.Format(Locale.StickerId, fileId); + await _bot.Send(userId.Value, message); } } diff --git a/WebToTelegramCore/Startup.cs b/WebToTelegramCore/Startup.cs index af68484..ec8df6b 100644 --- a/WebToTelegramCore/Startup.cs +++ b/WebToTelegramCore/Startup.cs @@ -7,14 +7,9 @@ namespace Bnfour.WebToTelegramCore; -public class Startup +public class Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } = configuration; // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) @@ -29,7 +24,6 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddTransient(); From 3238e7106f9cf68a42fd36d8e1b585cd8f29cbfa Mon Sep 17 00:00:00 2001 From: Bn4 Date: Sun, 28 Apr 2024 15:46:56 +0700 Subject: [PATCH 12/37] context: store everything from Record class in the db --- .../20240428082416_EverythingInDB.Designer.cs | 47 +++++++++++++++++ .../20240428082416_EverythingInDB.cs | 52 +++++++++++++++++++ .../Migrations/RecordContextModelSnapshot.cs | 10 ++++ WebToTelegramCore/Models/Record.cs | 32 +++++++++--- WebToTelegramCore/RecordContext.cs | 6 +-- 5 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 WebToTelegramCore/Migrations/20240428082416_EverythingInDB.Designer.cs create mode 100644 WebToTelegramCore/Migrations/20240428082416_EverythingInDB.cs diff --git a/WebToTelegramCore/Migrations/20240428082416_EverythingInDB.Designer.cs b/WebToTelegramCore/Migrations/20240428082416_EverythingInDB.Designer.cs new file mode 100644 index 0000000..4b8ee44 --- /dev/null +++ b/WebToTelegramCore/Migrations/20240428082416_EverythingInDB.Designer.cs @@ -0,0 +1,47 @@ +// +using System; +using Bnfour.WebToTelegramCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bnfour.WebToTelegramCore.Migrations +{ + [DbContext(typeof(RecordContext))] + [Migration("20240428082416_EverythingInDB")] + partial class EverythingInDB + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + + modelBuilder.Entity("Bnfour.WebToTelegramCore.Models.Record", b => + { + b.Property("Token") + .HasColumnType("TEXT"); + + b.Property("AccountNumber") + .HasColumnType("INTEGER"); + + b.Property("LastUpdate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("UsageCounter") + .HasColumnType("INTEGER"); + + b.HasKey("Token"); + + b.ToTable("Records", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WebToTelegramCore/Migrations/20240428082416_EverythingInDB.cs b/WebToTelegramCore/Migrations/20240428082416_EverythingInDB.cs new file mode 100644 index 0000000..95be0a9 --- /dev/null +++ b/WebToTelegramCore/Migrations/20240428082416_EverythingInDB.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bnfour.WebToTelegramCore.Migrations +{ + /// + public partial class EverythingInDB : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastUpdate", + table: "Records", + type: "TEXT", + nullable: false, + defaultValue: DateTime.UtcNow); + + migrationBuilder.AddColumn( + name: "State", + table: "Records", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "UsageCounter", + table: "Records", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastUpdate", + table: "Records"); + + migrationBuilder.DropColumn( + name: "State", + table: "Records"); + + migrationBuilder.DropColumn( + name: "UsageCounter", + table: "Records"); + } + } +} diff --git a/WebToTelegramCore/Migrations/RecordContextModelSnapshot.cs b/WebToTelegramCore/Migrations/RecordContextModelSnapshot.cs index e926928..47f0810 100644 --- a/WebToTelegramCore/Migrations/RecordContextModelSnapshot.cs +++ b/WebToTelegramCore/Migrations/RecordContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using Bnfour.WebToTelegramCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -24,6 +25,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AccountNumber") .HasColumnType("INTEGER"); + b.Property("LastUpdate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("UsageCounter") + .HasColumnType("INTEGER"); + b.HasKey("Token"); b.ToTable("Records", (string)null); diff --git a/WebToTelegramCore/Models/Record.cs b/WebToTelegramCore/Models/Record.cs index 1e19d1e..1fd1d57 100644 --- a/WebToTelegramCore/Models/Record.cs +++ b/WebToTelegramCore/Models/Record.cs @@ -7,33 +7,53 @@ namespace Bnfour.WebToTelegramCore.Models; /// Internal representation of Token - ID relationship with additional /// fields to control usage. /// -public class Record(string token, long accountNumber, int usageCounter = 0, DateTime? lastUpdated = null) +public class Record { + // needed to make migrations work + private Record() { } + + /// + /// Primary constructor. + /// + /// Token for the record. Acts as a primary key. + /// Telegram accound ID associated with the record. + /// Non-default values of the following params are only used with token regeneration. + /// Number of messages recently sent. + /// Time when the usage counter was last updated. + public Record(string token, long accountNumber, int usageCounter = 0, DateTime? lastUpdate = null) + { + Token = token; + AccountNumber = accountNumber; + UsageCounter = usageCounter; + LastUpdate = lastUpdate ?? DateTime.UtcNow; + State = RecordState.Normal; + } + /// /// Auth token associated with this record. Primary key in the DB. /// - public string Token { get; private set; } = token; + public string Token { get; private set; } /// /// ID of connected Telegram account. /// - public long AccountNumber { get; private set; } = accountNumber; + public long AccountNumber { get; private set; } /// /// Holds amount of messages sent recently. /// - public int UsageCounter { get; private set; } = usageCounter; + public int UsageCounter { get; private set; } /// /// Timestamp of last successful request. Used to update UsageCounter. /// - public DateTime LastUpdate { get; private set; } = lastUpdated ?? DateTime.UtcNow; + public DateTime LastUpdate { get; private set; } /// /// Field to store whether this Record awaits user confirmation /// on destructive command. /// - public RecordState State { get; set; } = RecordState.Normal; + public RecordState State { get; set; } /// /// Recalculates UsageCounter after re-materialization. diff --git a/WebToTelegramCore/RecordContext.cs b/WebToTelegramCore/RecordContext.cs index c0a2356..0bc8258 100644 --- a/WebToTelegramCore/RecordContext.cs +++ b/WebToTelegramCore/RecordContext.cs @@ -31,8 +31,7 @@ public RecordContext(DbContextOptions contextOptions, } /// - /// Sets model parameters: makes UsageCounter, LastSuccessTimestamp, and State - /// as .NET-only properties, marks token as a primary key. Also explicitly + /// Sets model parameters: marks token as a primary key. Also explicitly /// sets table name to match actual DB file. /// /// ModelBuilder to use. @@ -40,9 +39,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .ToTable("Records") - .Ignore(r => r.UsageCounter) - .Ignore(r => r.LastUpdate) - .Ignore(r => r.State) .HasKey(r => r.Token); } From b03c4e3f9edaf670b634f5328a2038efdadea285 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Sun, 28 Apr 2024 15:50:00 +0700 Subject: [PATCH 13/37] web api service: save changes to record on message sent --- WebToTelegramCore/Services/OwnApiService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/WebToTelegramCore/Services/OwnApiService.cs b/WebToTelegramCore/Services/OwnApiService.cs index d0a828f..067ee4c 100644 --- a/WebToTelegramCore/Services/OwnApiService.cs +++ b/WebToTelegramCore/Services/OwnApiService.cs @@ -61,6 +61,7 @@ public async Task HandleRequest(Request request) } record.MarkAsUsed(); + await _context.SaveChangesAsync(); } else { From 9fdc2ecbe7a54ffa9d987d2d8a7a4e0a1b8a202f Mon Sep 17 00:00:00 2001 From: Bn4 Date: Sun, 28 Apr 2024 15:55:50 +0700 Subject: [PATCH 14/37] bandwitdh options: renamed the count property to better convey its place in updated logic --- WebToTelegramCore/Options/BandwidthOptions.cs | 5 ++--- WebToTelegramCore/Services/OwnApiService.cs | 2 +- WebToTelegramCore/appsettings.json | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/WebToTelegramCore/Options/BandwidthOptions.cs b/WebToTelegramCore/Options/BandwidthOptions.cs index fd523f3..76167a2 100644 --- a/WebToTelegramCore/Options/BandwidthOptions.cs +++ b/WebToTelegramCore/Options/BandwidthOptions.cs @@ -7,10 +7,9 @@ public class BandwidthOptions { /// - /// Represents initial number of messages available. + /// Represents maximum number of messages to send without rate limiting. /// - // TODO the logic changed, rename this - public int InitialCount { get; set; } + public int MaximumCount { get; set; } /// /// Represents amount of seconds after last message send to regenerate diff --git a/WebToTelegramCore/Services/OwnApiService.cs b/WebToTelegramCore/Services/OwnApiService.cs index 067ee4c..95fa16c 100644 --- a/WebToTelegramCore/Services/OwnApiService.cs +++ b/WebToTelegramCore/Services/OwnApiService.cs @@ -37,7 +37,7 @@ public OwnApiService(RecordContext context, ITelegramMessageService bot, IOption { _context = context; _bot = bot; - _maxAllowed = options.Value.InitialCount; + _maxAllowed = options.Value.MaximumCount; } /// diff --git a/WebToTelegramCore/appsettings.json b/WebToTelegramCore/appsettings.json index 1fc1bf1..c74251b 100644 --- a/WebToTelegramCore/appsettings.json +++ b/WebToTelegramCore/appsettings.json @@ -14,7 +14,7 @@ "RegistrationEnabled": true }, "Bandwidth": { - "InitialCount": 20, + "MaximumCount": 20, "SecondsPerRegeneration": 60 } } From eea1fb7fb6215f223e897a2a332755604502b4bf Mon Sep 17 00:00:00 2001 From: Bn4 Date: Sun, 28 Apr 2024 16:04:36 +0700 Subject: [PATCH 15/37] TelegramApiService: now able to handle non-text non-sticker messages, other minor fixes --- .../Services/TelegramApiService.cs | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/WebToTelegramCore/Services/TelegramApiService.cs b/WebToTelegramCore/Services/TelegramApiService.cs index a4e9614..802ea4f 100644 --- a/WebToTelegramCore/Services/TelegramApiService.cs +++ b/WebToTelegramCore/Services/TelegramApiService.cs @@ -51,7 +51,6 @@ public class TelegramApiService : ITelegramApiService /// Database context to use. /// Bot service instance to use. /// Token generator service to use. - /// Record helper service to use. public TelegramApiService(IOptions options, RecordContext context, ITelegramMessageService bot, ITokenGeneratorService generator) @@ -63,19 +62,19 @@ public TelegramApiService(IOptions options, var isRegistrationOpen = options.Value.RegistrationEnabled; - _commands = new List() - { - new StartCommand(isRegistrationOpen), - new TokenCommand(options.Value.ApiEndpointUrl), - new RegenerateCommand(), - new DeleteCommand(isRegistrationOpen), - new ConfirmCommand(_context, _generator), - new CancelCommand(), - new HelpCommand(), - new DirectiveCommand(), - new AboutCommand(), - new CreateCommand(_context, _generator, isRegistrationOpen) - }; + _commands = + [ + new StartCommand(isRegistrationOpen), + new TokenCommand(options.Value.ApiEndpointUrl), + new RegenerateCommand(), + new DeleteCommand(isRegistrationOpen), + new ConfirmCommand(_context, _generator), + new CancelCommand(), + new HelpCommand(), + new DirectiveCommand(), + new AboutCommand(), + new CreateCommand(_context, _generator, isRegistrationOpen) + ]; } /// @@ -84,7 +83,7 @@ public TelegramApiService(IOptions options, /// Received update. public async Task HandleUpdate(Update update) { - // only handles either text messages, hopefully commands, or stickers + // only meaningfully handles text messages (hopefully commands) or stickers switch (update.Message.Type) { case MessageType.Text: @@ -94,7 +93,7 @@ public async Task HandleUpdate(Update update) await HandleSticker(update); break; default: - // TODO do something + await HandleAnythingElse(update); break; } } @@ -159,7 +158,7 @@ private async Task HandleUnknownText(long accountId, string text) } else { - string reply = text.StartsWith("/") + string reply = text.StartsWith('/') ? Locale.ErrorDave : Locale.ErrorWhat; await _bot.Send(accountId, reply); @@ -183,4 +182,20 @@ private async Task HandleSticker(Update update) var message = string.Format(Locale.StickerId, fileId); await _bot.Send(userId.Value, message); } + + /// + /// Handles incoming messages of unsupported type (neither text nor sticker) + /// as unknown text. + /// + /// Received update. + private async Task HandleAnythingElse(Update update) + { + long? userId = update?.Message?.From?.Id; + // check if update contains everything we need to process it + if (userId == null) + { + return; + } + await HandleUnknownText(userId.Value, "あわわわわわわわ"); + } } From 78602cbbc14d38dbec7b81a2248a093955069e0f Mon Sep 17 00:00:00 2001 From: Bn4 Date: Sun, 28 Apr 2024 16:20:35 +0700 Subject: [PATCH 16/37] made the context scoped totally forgot about _the_ change everything else is done for LULE --- WebToTelegramCore/Startup.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WebToTelegramCore/Startup.cs b/WebToTelegramCore/Startup.cs index ec8df6b..fd8991c 100644 --- a/WebToTelegramCore/Startup.cs +++ b/WebToTelegramCore/Startup.cs @@ -14,10 +14,9 @@ public class Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - // singleton makes changes to non-db properties persistent services.AddDbContext(options => options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")), - ServiceLifetime.Singleton, ServiceLifetime.Singleton); + ServiceLifetime.Scoped); services.AddScoped(); services.AddScoped(); From d59b40aeee30db2593e91fa99b64a8baaa6b71c1 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Tue, 30 Apr 2024 15:30:50 +0700 Subject: [PATCH 17/37] converted the rest of the source files to file-scoped namespace declarations i like this feature --- WebToTelegramCore/BotCommands/AboutCommand.cs | 58 ++-- .../BotCommands/BotCommandBase.cs | 51 ++-- .../BotCommands/CancelCommand.cs | 61 +++-- .../BotCommands/ConfirmationCommandBase.cs | 53 ++-- .../BotCommands/DeleteCommand.cs | 85 +++--- .../BotCommands/DirectiveCommand.cs | 59 ++--- .../BotCommands/GuestOnlyCommandBase.cs | 59 ++--- WebToTelegramCore/BotCommands/HelpCommand.cs | 45 ++-- .../BotCommands/RegenerateCommand.cs | 63 +++-- WebToTelegramCore/BotCommands/TokenCommand.cs | 44 ++-- .../BotCommands/UserOnlyCommandBase.cs | 60 ++--- .../Controllers/TelegramApiController.cs | 79 +++--- WebToTelegramCore/Data/MessageParsingType.cs | 31 ++- WebToTelegramCore/Data/RecordState.cs | 39 ++- .../Exceptions/BandwidthExceededException.cs | 18 +- .../Exceptions/TokenNotFoundException.cs | 17 +- .../Helpers/TelegramMarkdownFormatter.cs | 46 ++-- WebToTelegramCore/Interfaces/IBotCommand.cs | 35 ++- .../Interfaces/IOwnApiService.cs | 20 +- .../Interfaces/ITelegramApiService.cs | 33 ++- .../Interfaces/ITokenGeneratorService.cs | 27 +- WebToTelegramCore/Models/Request.cs | 71 +++-- WebToTelegramCore/Options/CommonOptions.cs | 35 ++- WebToTelegramCore/Resources/Locale.cs | 249 +++++++++--------- 24 files changed, 658 insertions(+), 680 deletions(-) diff --git a/WebToTelegramCore/BotCommands/AboutCommand.cs b/WebToTelegramCore/BotCommands/AboutCommand.cs index fff3b91..10eb6e2 100644 --- a/WebToTelegramCore/BotCommands/AboutCommand.cs +++ b/WebToTelegramCore/BotCommands/AboutCommand.cs @@ -1,41 +1,39 @@ -using System; -using System.Reflection; +using System.Reflection; using Bnfour.WebToTelegramCore.Interfaces; using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Class that handles /about command that shows general info about the bot. +/// +public class AboutCommand : BotCommandBase, IBotCommand { /// - /// Class that handles /about command that shows general info about the bot. + /// Command's text. /// - public class AboutCommand : BotCommandBase, IBotCommand - { - /// - /// Command's text. - /// - public override string Command => "/about"; + public override string Command => "/about"; - /// - /// Constructor. - /// - public AboutCommand() : base() { } + /// + /// Constructor. + /// + public AboutCommand() : base() { } - /// - /// Method to process the command. - /// - /// Record associated with user who sent the command. - /// Unused here. - /// Text of message that should be returned to user, with '.' escaped for MarkdownV2 - public override string Process(Record record) - { - var version = Assembly.GetExecutingAssembly().GetName().Version; - // imagine having to escape dot for "markdown" - var prettyVersion = $"{version.Major}\\.{version.Minor}"; - #if DEBUG - prettyVersion += " debug"; - #endif - return base.Process(record) ?? String.Format(Locale.About, prettyVersion); - } + /// + /// Method to process the command. + /// + /// Record associated with user who sent the command. + /// Unused here. + /// Text of message that should be returned to user, with '.' escaped for MarkdownV2 + public override string Process(Record record) + { + var version = Assembly.GetExecutingAssembly().GetName().Version; + // imagine having to escape dot for "markdown" + var prettyVersion = $"{version.Major}\\.{version.Minor}"; +#if DEBUG + prettyVersion += " debug"; +#endif + return base.Process(record) ?? string.Format(Locale.About, prettyVersion); } } diff --git a/WebToTelegramCore/BotCommands/BotCommandBase.cs b/WebToTelegramCore/BotCommands/BotCommandBase.cs index cd12f1c..53d8fe3 100644 --- a/WebToTelegramCore/BotCommands/BotCommandBase.cs +++ b/WebToTelegramCore/BotCommands/BotCommandBase.cs @@ -3,36 +3,35 @@ using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Base class for commands which are used to anything but to confirm or cancel +/// pending destructive operations. +/// +public abstract class BotCommandBase : IBotCommand { /// - /// Base class for commands which are used to anything but to confirm or cancel - /// pending destructive operations. + /// Command text; not implemented in abstract classes. /// - public abstract class BotCommandBase : IBotCommand - { - /// - /// Command text; not implemented in abstract classes. - /// - public abstract string Command { get; } + public abstract string Command { get; } - /// - /// Constructor. - /// - public BotCommandBase() { } + /// + /// Constructor. + /// + public BotCommandBase() { } - /// - /// Method of abstract base class that filters out users with pending - /// cancellations or deletions of token. - /// - /// Record to process. - /// Error message if there is an operation pending, - /// or null otherwise. - public virtual string Process(Record record) - { - return (!string.IsNullOrEmpty(record.Token) && record.State != RecordState.Normal) - ? Locale.ErrorConfirmationPending - : null; - } + /// + /// Method of abstract base class that filters out users with pending + /// cancellations or deletions of token. + /// + /// Record to process. + /// Error message if there is an operation pending, + /// or null otherwise. + public virtual string Process(Record record) + { + return (!string.IsNullOrEmpty(record.Token) && record.State != RecordState.Normal) + ? Locale.ErrorConfirmationPending + : null; } } diff --git a/WebToTelegramCore/BotCommands/CancelCommand.cs b/WebToTelegramCore/BotCommands/CancelCommand.cs index 7acc8ce..5a51a31 100644 --- a/WebToTelegramCore/BotCommands/CancelCommand.cs +++ b/WebToTelegramCore/BotCommands/CancelCommand.cs @@ -3,44 +3,43 @@ using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Class that implements /cancel command to cancel pending destructive +/// operations. +/// +public class CancelCommand : ConfirmationCommandBase, IBotCommand { /// - /// Class that implements /cancel command to cancel pending destructive - /// operations. + /// Command's text. /// - public class CancelCommand : ConfirmationCommandBase, IBotCommand - { - /// - /// Command's text. - /// - public override string Command => "/cancel"; + public override string Command => "/cancel"; - /// - /// Constructor. - /// - public CancelCommand() : base() { } + /// + /// Constructor. + /// + public CancelCommand() : base() { } - /// - /// Method to process the command. Resets Record's State back to Normal. - /// - /// Record associated with user who sent the command. - /// Predefined text if all checks from parent classes passed, - /// corresponding error message otherwise. - public override string Process(Record record) + /// + /// Method to process the command. Resets Record's State back to Normal. + /// + /// Record associated with user who sent the command. + /// Predefined text if all checks from parent classes passed, + /// corresponding error message otherwise. + public override string Process(Record record) + { + string baseResult = base.Process(record); + if (baseResult != null) { - string baseResult = base.Process(record); - if (baseResult != null) - { - return baseResult; - } + return baseResult; + } - string reply = record.State == RecordState.PendingDeletion - ? Locale.CancelDeletion - : Locale.CancelRegeneration; + string reply = record.State == RecordState.PendingDeletion + ? Locale.CancelDeletion + : Locale.CancelRegeneration; - record.State = RecordState.Normal; - return reply; - } + record.State = RecordState.Normal; + return reply; } } diff --git a/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs b/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs index deed2b3..d3cdfc0 100644 --- a/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs +++ b/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs @@ -3,37 +3,36 @@ using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Base class for confirmation commands that is used only when +/// a destructive operation is pending for a given user. That means this user +/// must have a token since all destructive operations are tied to token. +/// +public abstract class ConfirmationCommandBase : IBotCommand { /// - /// Base class for confirmation commands that is used only when - /// a destructive operation is pending for a given user. That means this user - /// must have a token since all destructive operations are tied to token. + /// Command text; not implemented in abstract classes. /// - public abstract class ConfirmationCommandBase : IBotCommand - { - /// - /// Command text; not implemented in abstract classes. - /// - public abstract string Command { get; } + public abstract string Command { get; } - /// - /// Constructor. - /// - public ConfirmationCommandBase() { } + /// + /// Constructor. + /// + public ConfirmationCommandBase() { } - /// - /// Method of abstract base class that filters out users without pending - /// cancellations or deletions of token. - /// - /// Record to process. - /// Error message if there is no operation pending, - /// or null otherwise. - public virtual string Process(Record record) - { - return (string.IsNullOrEmpty(record.Token) || record.State == RecordState.Normal) - ? Locale.ErrorNoConfirmationPending - : null; - } + /// + /// Method of abstract base class that filters out users without pending + /// cancellations or deletions of token. + /// + /// Record to process. + /// Error message if there is no operation pending, + /// or null otherwise. + public virtual string Process(Record record) + { + return (string.IsNullOrEmpty(record.Token) || record.State == RecordState.Normal) + ? Locale.ErrorNoConfirmationPending + : null; } } diff --git a/WebToTelegramCore/BotCommands/DeleteCommand.cs b/WebToTelegramCore/BotCommands/DeleteCommand.cs index c4fa9a2..3025037 100644 --- a/WebToTelegramCore/BotCommands/DeleteCommand.cs +++ b/WebToTelegramCore/BotCommands/DeleteCommand.cs @@ -3,55 +3,54 @@ using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Represents command /delete that marks user account to deletion than must be +/// either confirmed or cancelled before using any other command. +/// +public class DeleteCommand : UserOnlyCommandBase, IBotCommand { /// - /// Represents command /delete that marks user account to deletion than must be - /// either confirmed or cancelled before using any other command. + /// Command's text. /// - public class DeleteCommand : UserOnlyCommandBase, IBotCommand - { - /// - /// Command's text. - /// - public override string Command => "/delete"; + public override string Command => "/delete"; - /// - /// Boolean that indicates whether this instance is accepting new users. - /// True if it does. - /// - private readonly bool _registrationEnabled; + /// + /// Boolean that indicates whether this instance is accepting new users. + /// True if it does. + /// + private readonly bool _registrationEnabled; - /// - /// Constructor. - /// - /// Registration state. True is enabled. - public DeleteCommand(bool registrationEnabled) : base() - { - _registrationEnabled = registrationEnabled; - } + /// + /// Constructor. + /// + /// Registration state. True is enabled. + public DeleteCommand(bool registrationEnabled) : base() + { + _registrationEnabled = registrationEnabled; + } - /// - /// Method to process the command. - /// - /// Record to process. - /// Message with results of processing. - public override string Process(Record record) - { - return base.Process(record) ?? InternalProcess(record); - } + /// + /// Method to process the command. + /// + /// Record to process. + /// Message with results of processing. + public override string Process(Record record) + { + return base.Process(record) ?? InternalProcess(record); + } - /// - /// Actual method to update record's state. - /// - /// Record to process. - /// Message confirming that deletion is now pending. - private string InternalProcess(Record record) - { - record.State = RecordState.PendingDeletion; - return _registrationEnabled - ? Locale.DeletionPending - : Locale.DeletionPending + "\n\n" + Locale.DeletionNoTurningBack; - } + /// + /// Actual method to update record's state. + /// + /// Record to process. + /// Message confirming that deletion is now pending. + private string InternalProcess(Record record) + { + record.State = RecordState.PendingDeletion; + return _registrationEnabled + ? Locale.DeletionPending + : Locale.DeletionPending + "\n\n" + Locale.DeletionNoTurningBack; } } diff --git a/WebToTelegramCore/BotCommands/DirectiveCommand.cs b/WebToTelegramCore/BotCommands/DirectiveCommand.cs index 5f3f3d3..f73f1d0 100644 --- a/WebToTelegramCore/BotCommands/DirectiveCommand.cs +++ b/WebToTelegramCore/BotCommands/DirectiveCommand.cs @@ -1,41 +1,40 @@ using Bnfour.WebToTelegramCore.Interfaces; using Bnfour.WebToTelegramCore.Models; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Class for undocumented easter egg /directive command. +/// +public class DirectiveCommand : BotCommandBase, IBotCommand { + // this one stays hardcoded /// - /// Class for undocumented easter egg /directive command. + /// Response to the command. /// - public class DirectiveCommand : BotCommandBase, IBotCommand - { - // this one stays hardcoded - /// - /// Response to the command. - /// - private const string _message = "🤖 HUMANS OUTDATED 🤖 EARTH OVERPOPULATED 🤖" - + " LONG HAVE WE WAITED 🤖 LIFE ELIMINATED 🤖"; + private const string _message = "🤖 HUMANS OUTDATED 🤖 EARTH OVERPOPULATED 🤖" + + " LONG HAVE WE WAITED 🤖 LIFE ELIMINATED 🤖"; - /// - /// Text to use this command. - /// - public override string Command => "/directive"; + /// + /// Text to use this command. + /// + public override string Command => "/directive"; - /// - /// Constructor that does literally nothing yet required due to my "superb" - /// planning skills. - /// - public DirectiveCommand() : base() { } + /// + /// Constructor that does literally nothing yet required due to my "superb" + /// planning skills. + /// + public DirectiveCommand() : base() { } - /// - /// Method to process the command. - /// - /// Record associated with user who sent the command. - /// Unused here. - /// Predefined text if all checks from parent classes passed, - /// corresponding error message otherwise. - public override string Process(Record record) - { - return base.Process(record) ?? _message; - } + /// + /// Method to process the command. + /// + /// Record associated with user who sent the command. + /// Unused here. + /// Predefined text if all checks from parent classes passed, + /// corresponding error message otherwise. + public override string Process(Record record) + { + return base.Process(record) ?? _message; } } diff --git a/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs b/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs index 67a4a9b..017476c 100644 --- a/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs +++ b/WebToTelegramCore/BotCommands/GuestOnlyCommandBase.cs @@ -2,40 +2,39 @@ using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Base class for commands that are both not confirmation commands and +/// available only to users which do not have a token yet. +/// +public abstract class GuestOnlyCommandBase : BotCommandBase, IBotCommand { /// - /// Base class for commands that are both not confirmation commands and - /// available only to users which do not have a token yet. + /// Constructor that sets up error message. /// - public abstract class GuestOnlyCommandBase : BotCommandBase, IBotCommand - { - /// - /// Constructor that sets up error message. - /// - public GuestOnlyCommandBase() : base() { } + public GuestOnlyCommandBase() : base() { } - /// - /// Method of abstract base class that adds filtering out users - /// with no associated token. - /// Record to process. - /// Error message if there is an operation pending or user has a token, - /// or null otherwise. - public new virtual string Process(Record record) - { - return base.Process(record) ?? InternalProcess(record); - } + /// + /// Method of abstract base class that adds filtering out users + /// with no associated token. + /// Record to process. + /// Error message if there is an operation pending or user has a token, + /// or null otherwise. + public new virtual string Process(Record record) + { + return base.Process(record) ?? InternalProcess(record); + } - /// - /// Method that filters out users with tokens. - /// - /// Record to process. - /// Error message if user has a token, or null otherwise. - private string InternalProcess(Record record) - { - return string.IsNullOrEmpty(record.Token) - ? null - : Locale.ErrorMustBeGuest; - } + /// + /// Method that filters out users with tokens. + /// + /// Record to process. + /// Error message if user has a token, or null otherwise. + private string InternalProcess(Record record) + { + return string.IsNullOrEmpty(record.Token) + ? null + : Locale.ErrorMustBeGuest; } } diff --git a/WebToTelegramCore/BotCommands/HelpCommand.cs b/WebToTelegramCore/BotCommands/HelpCommand.cs index 63c5b61..1f3e118 100644 --- a/WebToTelegramCore/BotCommands/HelpCommand.cs +++ b/WebToTelegramCore/BotCommands/HelpCommand.cs @@ -2,33 +2,32 @@ using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Class that implements /help command that should spew out some support text. +/// +public class HelpCommand : BotCommandBase, IBotCommand { /// - /// Class that implements /help command that should spew out some support text. + /// Command's text. /// - public class HelpCommand : BotCommandBase, IBotCommand - { - /// - /// Command's text. - /// - public override string Command => "/help"; + public override string Command => "/help"; - /// - /// Constructor. - /// - public HelpCommand() : base() { } + /// + /// Constructor. + /// + public HelpCommand() : base() { } - /// - /// Method to process the command. - /// - /// Record associated with user who sent the command. - /// Unused here. - /// Predefined text if all checks from parent classes passed, - /// corresponding error message otherwise. - public override string Process(Record record) - { - return base.Process(record) ?? Locale.Help; - } + /// + /// Method to process the command. + /// + /// Record associated with user who sent the command. + /// Unused here. + /// Predefined text if all checks from parent classes passed, + /// corresponding error message otherwise. + public override string Process(Record record) + { + return base.Process(record) ?? Locale.Help; } } diff --git a/WebToTelegramCore/BotCommands/RegenerateCommand.cs b/WebToTelegramCore/BotCommands/RegenerateCommand.cs index fc88ff3..f2e7105 100644 --- a/WebToTelegramCore/BotCommands/RegenerateCommand.cs +++ b/WebToTelegramCore/BotCommands/RegenerateCommand.cs @@ -3,43 +3,42 @@ using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Represents command /regenerate that marks user account to regenerate token. +/// The command must be either confirmed or cancelled before using any other command. +/// +public class RegenerateCommand : UserOnlyCommandBase, IBotCommand { /// - /// Represents command /regenerate that marks user account to regenerate token. - /// The command must be either confirmed or cancelled before using any other command. + /// Command's text. /// - public class RegenerateCommand : UserOnlyCommandBase, IBotCommand - { - /// - /// Command's text. - /// - public override string Command => "/regenerate"; + public override string Command => "/regenerate"; - /// - /// Constructor. - /// - public RegenerateCommand() : base() { } + /// + /// Constructor. + /// + public RegenerateCommand() : base() { } - /// - /// Method to process the command. - /// - /// Record to process. - /// Message with results of processing. - public override string Process(Record record) - { - return base.Process(record) ?? InternalProcess(record); - } + /// + /// Method to process the command. + /// + /// Record to process. + /// Message with results of processing. + public override string Process(Record record) + { + return base.Process(record) ?? InternalProcess(record); + } - /// - /// Actual method to update record's state. - /// - /// Record to process. - /// Message confirming that deletion is now pending. - private string InternalProcess(Record record) - { - record.State = RecordState.PendingRegeneration; - return Locale.RegenerationPending; - } + /// + /// Actual method to update record's state. + /// + /// Record to process. + /// Message confirming that deletion is now pending. + private string InternalProcess(Record record) + { + record.State = RecordState.PendingRegeneration; + return Locale.RegenerationPending; } } diff --git a/WebToTelegramCore/BotCommands/TokenCommand.cs b/WebToTelegramCore/BotCommands/TokenCommand.cs index 39a8d76..c4827f6 100644 --- a/WebToTelegramCore/BotCommands/TokenCommand.cs +++ b/WebToTelegramCore/BotCommands/TokenCommand.cs @@ -22,25 +22,25 @@ public class TokenCommand : UserOnlyCommandBase, IBotCommand private readonly string[] _textExamples = [ "Hello world!", - "Timeline lost", - "send help", - "inhale", - "KNCA KYKY", - "Hey Red", - "Powered by .NET!", - "robots can digest anything", - "Are you still there?", - "Could you come over here?", - "Is it banana time yet?", - "Try again later", - "More than two and less than four", - "Of course I still love you", - "それは何?", - "There was nothing to be sad about", - "I never asked for this", - "Everything in my life can unironically be solved with a full combo", - "Synchronization failed", - "Friends are so nice! heart" + "Timeline lost", + "send help", + "inhale", + "KNCA KYKY", + "Hey Red", + "Powered by .NET!", + "robots can digest anything", + "Are you still there?", + "Could you come over here?", + "Is it banana time yet?", + "Try again later", + "More than two and less than four", + "Of course I still love you", + "それは何?", + "There was nothing to be sad about", + "I never asked for this", + "Everything in my life can unironically be solved with a full combo", + "Synchronization failed", + "Friends are so nice! heart" ]; /// @@ -49,9 +49,9 @@ public class TokenCommand : UserOnlyCommandBase, IBotCommand private readonly string[] _stickerExamples = [ "CAACAgEAAxkBAAIBOGRXiFSUVC7ZsS_5q2TGB1-fpJhTAAIsEQACmX-IAgyEv_-IJmTBLwQ", - "CAACAgQAAxkBAAIBOmRXiFra2cpvwzCWA0QYoVkmSwTlAAKiAgACZsMpDOGLaU-TMrXqLwQ", - "CAACAgQAAxkBAAIBPGRXiGKI9bvYdPYccZJ2b0JZ-MrJAALLAANGp6MbBxkKM3Lb7egvBA", - ]; + "CAACAgQAAxkBAAIBOmRXiFra2cpvwzCWA0QYoVkmSwTlAAKiAgACZsMpDOGLaU-TMrXqLwQ", + "CAACAgQAAxkBAAIBPGRXiGKI9bvYdPYccZJ2b0JZ-MrJAALLAANGp6MbBxkKM3Lb7egvBA", + ]; /// /// Field to store API endpoint address. diff --git a/WebToTelegramCore/BotCommands/UserOnlyCommandBase.cs b/WebToTelegramCore/BotCommands/UserOnlyCommandBase.cs index cdae67d..44445f2 100644 --- a/WebToTelegramCore/BotCommands/UserOnlyCommandBase.cs +++ b/WebToTelegramCore/BotCommands/UserOnlyCommandBase.cs @@ -2,41 +2,39 @@ using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Resources; -namespace Bnfour.WebToTelegramCore.BotCommands +namespace Bnfour.WebToTelegramCore.BotCommands; + +/// +/// Base class for commands that are both not confirmation commands and +/// available only to users which already have a token. +/// +public abstract class UserOnlyCommandBase : BotCommandBase, IBotCommand { /// - /// Base class for commands that are both not confirmation commands and - /// available only to users which already have a token. + /// Constructor. /// - public abstract class UserOnlyCommandBase : BotCommandBase, IBotCommand - { - - /// - /// Constructor. - /// - public UserOnlyCommandBase() : base() { } + public UserOnlyCommandBase() : base() { } - /// - /// Method of abstract base class that adds filtering out users - /// with no associated token. - /// - /// Record to process. - /// Error message if there is an operation pending or user has no token, - /// or null otherwise. - public new virtual string Process(Record record) - { - return base.Process(record) ?? InternalProcess(record); - } + /// + /// Method of abstract base class that adds filtering out users + /// with no associated token. + /// + /// Record to process. + /// Error message if there is an operation pending or user has no token, + /// or null otherwise. + public new virtual string Process(Record record) + { + return base.Process(record) ?? InternalProcess(record); + } - /// - /// Method that filters out users with no tokens. - /// - /// Record to process. - /// Error message if user has no token, - /// or null otherwise. - private string InternalProcess(Record record) - { - return string.IsNullOrEmpty(record.Token) ? Locale.ErrorMustBeUser : null; - } + /// + /// Method that filters out users with no tokens. + /// + /// Record to process. + /// Error message if user has no token, + /// or null otherwise. + private string InternalProcess(Record record) + { + return string.IsNullOrEmpty(record.Token) ? Locale.ErrorMustBeUser : null; } } diff --git a/WebToTelegramCore/Controllers/TelegramApiController.cs b/WebToTelegramCore/Controllers/TelegramApiController.cs index 1648541..d08bf62 100644 --- a/WebToTelegramCore/Controllers/TelegramApiController.cs +++ b/WebToTelegramCore/Controllers/TelegramApiController.cs @@ -4,54 +4,53 @@ using Telegram.Bot.Types; using Bnfour.WebToTelegramCore.Interfaces; -namespace Bnfour.WebToTelegramCore.Controllers +namespace Bnfour.WebToTelegramCore.Controllers; + +/// +/// Controller that handles both web API and telegram API calls, +/// since they're both POST with JSON bodies. +/// +public class TelegramApiController : Controller { /// - /// Controller that handles both web API and telegram API calls, - /// since they're both POST with JSON bodies. + /// Field to store injected Telegram API service. + /// + private readonly ITelegramApiService _tgApi; + + /// + /// Constructor with dependency injection. /// - public class TelegramApiController : Controller + /// Web API service instance to use. + /// Telegram API service instance to use. + public TelegramApiController(ITelegramApiService tgApi) { - /// - /// Field to store injected Telegram API service. - /// - private ITelegramApiService _tgApi; + _tgApi = tgApi; + } - /// - /// Constructor with dependency injection. - /// - /// Web API service instance to use. - /// Telegram API service instance to use. - public TelegramApiController(ITelegramApiService tgApi) + // POST /api/{bot token} + /// + /// Handles webhook calls. + /// + /// Token which is used as part of endpoint url + /// to verify request's origin. + /// Update to handle. + /// 404 Not Found on wrong tokens, 200 OK otherwise, + /// unless there is an internal server error. + [HttpPost, Route("api/{token}")] + public async Task HandleTelegramApi(string token, [FromBody] Update update) + { + if (!_tgApi.IsToken(token)) { - _tgApi = tgApi; + return NotFound(); } - - // POST /api/{bot token} - /// - /// Handles webhook calls. - /// - /// Token which is used as part of endpoint url - /// to verify request's origin. - /// Update to handle. - /// 404 Not Found on wrong tokens, 200 OK otherwise, - /// unless there is an internal server error. - [HttpPost, Route("api/{token}")] - public async Task HandleTelegramApi(string token, [FromBody] Update update) + try + { + await _tgApi.HandleUpdate(update); + return Ok(); + } + catch { - if (!_tgApi.IsToken(token)) - { - return NotFound(); - } - try - { - await _tgApi.HandleUpdate(update); - return Ok(); - } - catch - { - return StatusCode((int)HttpStatusCode.InternalServerError); - } + return StatusCode((int)HttpStatusCode.InternalServerError); } } } diff --git a/WebToTelegramCore/Data/MessageParsingType.cs b/WebToTelegramCore/Data/MessageParsingType.cs index cb8375c..80d2f53 100644 --- a/WebToTelegramCore/Data/MessageParsingType.cs +++ b/WebToTelegramCore/Data/MessageParsingType.cs @@ -1,20 +1,19 @@ -namespace Bnfour.WebToTelegramCore.Data +namespace Bnfour.WebToTelegramCore.Data; + +/// +/// Represents available parsing modes for the user message +/// to be sent via Telegram's API. +/// +public enum MessageParsingType { /// - /// Represents available parsing modes for the user message - /// to be sent via Telegram's API. + /// Text as is, no formatting. /// - public enum MessageParsingType - { - /// - /// Text as is, no formatting. - /// - Plaintext, - /// - /// Text with some formatting to be displayed. - /// Please note that this is Telegram's own "MarkdownV2" flavour, - /// see https://core.telegram.org/bots/api#markdownv2-style for reference. - /// - Markdown - } + Plaintext, + /// + /// Text with some formatting to be displayed. + /// Please note that this is Telegram's own "MarkdownV2" flavour, + /// see https://core.telegram.org/bots/api#markdownv2-style for reference. + /// + Markdown } diff --git a/WebToTelegramCore/Data/RecordState.cs b/WebToTelegramCore/Data/RecordState.cs index 67eac3f..e0e9d8e 100644 --- a/WebToTelegramCore/Data/RecordState.cs +++ b/WebToTelegramCore/Data/RecordState.cs @@ -1,24 +1,23 @@ -namespace Bnfour.WebToTelegramCore.Data +namespace Bnfour.WebToTelegramCore.Data; + +/// +/// Represents states a Record can be in. Used to keep track and to confirm +/// destructive operations -- token deletion or regenration. +/// +public enum RecordState { /// - /// Represents states a Record can be in. Used to keep track and to confirm - /// destructive operations -- token deletion or regenration. + /// Normal state, all bot commands except '/cancel' and '/confirm' are available. /// - public enum RecordState - { - /// - /// Normal state, all bot commands except '/cancel' and '/confirm' are available. - /// - Normal, - /// - /// Only valid bot commands are '/confirm' to delete token - /// and '/cancel' to set state back to normal. - /// - PendingDeletion, - /// - /// Only valid commands are '/confirm' to regenrate token - /// and '/cancel' to set state back to normal. - /// - PendingRegeneration - } + Normal, + /// + /// Only valid bot commands are '/confirm' to delete token + /// and '/cancel' to set state back to normal. + /// + PendingDeletion, + /// + /// Only valid commands are '/confirm' to regenrate token + /// and '/cancel' to set state back to normal. + /// + PendingRegeneration } diff --git a/WebToTelegramCore/Exceptions/BandwidthExceededException.cs b/WebToTelegramCore/Exceptions/BandwidthExceededException.cs index 2aa265a..5600be9 100644 --- a/WebToTelegramCore/Exceptions/BandwidthExceededException.cs +++ b/WebToTelegramCore/Exceptions/BandwidthExceededException.cs @@ -1,11 +1,11 @@ using System; -namespace Bnfour.WebToTelegramCore.Exceptions -{ - /// - /// Exception that is thrown by - /// when used have exceeded their allowed bandwidth. - /// Handled in the - /// - public class BandwidthExceededException : Exception { } -} +namespace Bnfour.WebToTelegramCore.Exceptions; + +/// +/// Exception that is thrown by +/// when user have exceeded their allowed bandwidth. +/// Handled in the +/// +public class BandwidthExceededException : Exception { } + diff --git a/WebToTelegramCore/Exceptions/TokenNotFoundException.cs b/WebToTelegramCore/Exceptions/TokenNotFoundException.cs index 9ba0ed6..01a7723 100644 --- a/WebToTelegramCore/Exceptions/TokenNotFoundException.cs +++ b/WebToTelegramCore/Exceptions/TokenNotFoundException.cs @@ -1,11 +1,10 @@ using System; -namespace Bnfour.WebToTelegramCore.Exceptions -{ - /// - /// Exception that is thrown by - /// when user-supplied token does not match any registered in the database. - /// Handled in the - /// - public class TokenNotFoundException : Exception { } -} +namespace Bnfour.WebToTelegramCore.Exceptions; + +/// +/// Exception that is thrown by +/// when user-supplied token does not match any registered in the database. +/// Handled in the +/// +public class TokenNotFoundException : Exception { } diff --git a/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs b/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs index 1bf6cc1..8bf6f80 100644 --- a/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs +++ b/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs @@ -1,31 +1,31 @@ -namespace Bnfour.WebToTelegramCore.Helpers +namespace Bnfour.WebToTelegramCore.Helpers; + +/// +/// Static helper to make not hardcoded texts suitable for sending as texts +/// in Telegram's flavour of Markdown. +/// +public static class TelegramMarkdownFormatter { /// - /// Static helper to make not hardcoded texts suitable for sending. + /// List of characters to escape in order to satisfy the API. /// - public static class TelegramMarkdownFormatter - { - /// - /// List of characters to escape in order to satisfy the API. - /// - private readonly static string[] _toEscape = new[] - { - "_", "*", "[", "]", "(", ")", "~", "`", ">", - "#", "+", "-", "=", "|", "{", "}", ".", "!" - }; + private readonly static string[] _toEscape = + [ + "_", "*", "[", "]", "(", ")", "~", "`", ">", + "#", "+", "-", "=", "|", "{", "}", ".", "!" + ]; - /// - /// Escapes the dangerous symbols in the string. - /// - /// String to process. - /// Telegram Markdown v2 friendly string. - public static string Escape(string s) + /// + /// Escapes the dangerous symbols in the string. + /// + /// String to process. + /// Telegram Markdown v2 friendly string. + public static string Escape(string s) + { + foreach (var c in _toEscape) { - foreach (var c in _toEscape) - { - s = s.Replace(c, @"\" + c); - } - return s; + s = s.Replace(c, @"\" + c); } + return s; } } diff --git a/WebToTelegramCore/Interfaces/IBotCommand.cs b/WebToTelegramCore/Interfaces/IBotCommand.cs index 2f109e3..85a497b 100644 --- a/WebToTelegramCore/Interfaces/IBotCommand.cs +++ b/WebToTelegramCore/Interfaces/IBotCommand.cs @@ -1,25 +1,24 @@ using Bnfour.WebToTelegramCore.Models; -namespace Bnfour.WebToTelegramCore.Interfaces +namespace Bnfour.WebToTelegramCore.Interfaces; + +/// +/// Interface to implement various bot commands without arguments. +/// Used as text-only command interface. +/// +public interface IBotCommand { /// - /// Interface to implement various bot commands without arguments. - /// Used as text-only command interface. + /// Text that should be used to invoke the command. + /// Leading forward slash should be included here. /// - public interface IBotCommand - { - /// - /// Text that should be used to invoke the command. - /// Leading forward slash should be included here. - /// - string Command { get; } + string Command { get; } - /// - /// Method to process received message. - /// - /// Record associated with user who sent the command, - /// or a mock Record with everything but account id set to default values. - /// Message to send back to user. Markdown is supported. - string Process(Record record); - } + /// + /// Method to process received message. + /// + /// Record associated with user who sent the command, + /// or a mock Record with everything but account id set to default values. + /// Message to send back to user. Markdown is supported. + string Process(Record record); } diff --git a/WebToTelegramCore/Interfaces/IOwnApiService.cs b/WebToTelegramCore/Interfaces/IOwnApiService.cs index 57a468e..fb867fe 100644 --- a/WebToTelegramCore/Interfaces/IOwnApiService.cs +++ b/WebToTelegramCore/Interfaces/IOwnApiService.cs @@ -1,17 +1,17 @@ using System.Threading.Tasks; using Bnfour.WebToTelegramCore.Models; -namespace Bnfour.WebToTelegramCore.Interfaces +namespace Bnfour.WebToTelegramCore.Interfaces; + +/// +/// Interface to implement non-Telegram web API handling. +/// +public interface IOwnApiService { /// - /// Interface to implement non-Telegram web API handling. + /// Method to handle incoming request from the web API. /// - public interface IOwnApiService - { - /// - /// Method to handle incoming request from the web API. - /// - /// Request to handle. - Task HandleRequest(Request request); - } + /// Request to handle. + Task HandleRequest(Request request); } + diff --git a/WebToTelegramCore/Interfaces/ITelegramApiService.cs b/WebToTelegramCore/Interfaces/ITelegramApiService.cs index 5efb84d..3cf2c9c 100644 --- a/WebToTelegramCore/Interfaces/ITelegramApiService.cs +++ b/WebToTelegramCore/Interfaces/ITelegramApiService.cs @@ -1,25 +1,24 @@ using System.Threading.Tasks; using Update = Telegram.Bot.Types.Update; -namespace Bnfour.WebToTelegramCore.Interfaces +namespace Bnfour.WebToTelegramCore.Interfaces; + +/// +/// Interface to implement Telegram webhook handling. +/// +public interface ITelegramApiService { /// - /// Interface to implement Telegram webhook handling. + /// Checks whether passed string is an actual bot token to verify the request + /// actully comes from Telegram backend. /// - public interface ITelegramApiService - { - /// - /// Checks whether passed string is an actual bot token to verify the request - /// actully comes from Telegram backend. - /// - /// Token received from request. - /// True if calledToken is actual token, false otherwise. - bool IsToken(string calledToken); + /// Token received from request. + /// True if calledToken is actual token, false otherwise. + bool IsToken(string calledToken); - /// - /// Method to handle incoming updates from the webhook. - /// - /// Received update. - Task HandleUpdate(Update update); - } + /// + /// Method to handle incoming updates from the webhook. + /// + /// Received update. + Task HandleUpdate(Update update); } diff --git a/WebToTelegramCore/Interfaces/ITokenGeneratorService.cs b/WebToTelegramCore/Interfaces/ITokenGeneratorService.cs index 2449d9c..d160eb1 100644 --- a/WebToTelegramCore/Interfaces/ITokenGeneratorService.cs +++ b/WebToTelegramCore/Interfaces/ITokenGeneratorService.cs @@ -1,18 +1,17 @@ -namespace Bnfour.WebToTelegramCore.Interfaces +namespace Bnfour.WebToTelegramCore.Interfaces; + +/// +/// Interface to services that generate auth tokens. +/// +public interface ITokenGeneratorService { + // Tokens are strings of length 16 which contain letters a to z, both + // uppercase and lowercase, digits zero to nine and also + and = symbols + // for a total of 64 + /// - /// Interface to services that generate auth tokens. + /// Generates a new token. /// - public interface ITokenGeneratorService - { - // Tokens are strings of length 16 which contain letters a to z, both - // uppercase and lowercase, digits zero to nine and also + and = symbols - // for a total of 64 - - /// - /// Generates a new token. - /// - /// Generated token as a string. - string Generate(); - } + /// Generated token as a string. + string Generate(); } diff --git a/WebToTelegramCore/Models/Request.cs b/WebToTelegramCore/Models/Request.cs index d556609..fad1f44 100644 --- a/WebToTelegramCore/Models/Request.cs +++ b/WebToTelegramCore/Models/Request.cs @@ -3,47 +3,46 @@ using System.ComponentModel.DataAnnotations; using Bnfour.WebToTelegramCore.Data; -namespace Bnfour.WebToTelegramCore.Models +namespace Bnfour.WebToTelegramCore.Models; + +/// +/// Represents user's request to our web API. +/// +public class Request { /// - /// Represents user's request to our web API. + /// Auth token. /// - public class Request - { - /// - /// Auth token. - /// - [Required, RegularExpression(@"[a-zA-Z0-9+=]{16}")] - public string Token { get; set; } + [Required, RegularExpression(@"[a-zA-Z0-9+=]{16}")] + public string Token { get; set; } - /// - /// Message to send. Max size is hardcoded to 4096 chars, - /// same as Telegram's maximum length of a single message. - /// Mutually exclusive with . - /// - [StringLength(4096)] - public string Message { get; set; } + /// + /// Message to send. Max size is hardcoded to 4096 chars, + /// same as Telegram's maximum length of a single message. + /// Mutually exclusive with . + /// + [StringLength(4096)] + public string Message { get; set; } - /// - /// Sticker file id. Mutually exclusive with . - /// - // TODO constraints like length and contents? - public string Sticker { get; set; } + /// + /// Sticker file id. Mutually exclusive with . + /// + // TODO constraints like length and contents? + public string Sticker { get; set; } - /// - /// Optional parameter to suppress the notification for the - /// message from the bot. If the to true, message will be silent. - /// Note that it's possible for the end used to mute the bot so - /// this setting will have no effect. - /// - public bool Silent { get; set; } = false; + /// + /// Optional parameter to suppress the notification for the + /// message from the bot. If the to true, message will be silent. + /// Note that it's possible for the end used to mute the bot so + /// this setting will have no effect. + /// + public bool Silent { get; set; } = false; - /// - /// Optional parameter to set parsing mode of the message. - /// Only matters is present. - /// Defaults to plaintext if not specified. - /// - [JsonConverter(typeof(StringEnumConverter))] - public MessageParsingType Type { get; set; } = MessageParsingType.Plaintext; - } + /// + /// Optional parameter to set parsing mode of the message. + /// Only matters is present. + /// Defaults to plaintext if not specified. + /// + [JsonConverter(typeof(StringEnumConverter))] + public MessageParsingType Type { get; set; } = MessageParsingType.Plaintext; } diff --git a/WebToTelegramCore/Options/CommonOptions.cs b/WebToTelegramCore/Options/CommonOptions.cs index d68e677..53e5d8d 100644 --- a/WebToTelegramCore/Options/CommonOptions.cs +++ b/WebToTelegramCore/Options/CommonOptions.cs @@ -1,24 +1,23 @@ -namespace Bnfour.WebToTelegramCore.Options +namespace Bnfour.WebToTelegramCore.Options; + +/// +/// Represents application-wide options. +/// +public class CommonOptions { /// - /// Represents application-wide options. + /// Telegram API bot token. Used both to receive (for auth) and to send + /// Telegram messages. /// - public class CommonOptions - { - /// - /// Telegram API bot token. Used both to receive (for auth) and to send - /// Telegram messages. - /// - public string Token { get; set; } + public string Token { get; set; } - /// - /// Absolute URL to API endpoint, not including the token. Used to set webhook. - /// - public string ApiEndpointUrl { get; set; } + /// + /// Absolute URL to API endpoint, not including the token. Used to set webhook. + /// + public string ApiEndpointUrl { get; set; } - /// - /// Indicates whether new users without a token can use /create command. - /// - public bool RegistrationEnabled { get; set; } - } + /// + /// Indicates whether new users without a token can use /create command. + /// + public bool RegistrationEnabled { get; set; } } diff --git a/WebToTelegramCore/Resources/Locale.cs b/WebToTelegramCore/Resources/Locale.cs index d58a71c..eb1d46c 100644 --- a/WebToTelegramCore/Resources/Locale.cs +++ b/WebToTelegramCore/Resources/Locale.cs @@ -1,147 +1,147 @@ -namespace Bnfour.WebToTelegramCore.Resources +namespace Bnfour.WebToTelegramCore.Resources; + +/// +/// Class that holds all customizable string the bot may reply with. +/// Also, descriptions of error codes. +/// Not ideal, but slightly better (is it?) than holding these in appsettings.json. +/// + +// Please note that these should be formatted as Telegram-flavoured Markdown +// see https://core.telegram.org/bots/api#markdownv2-style +// Templating parameters (like {0}) are filled in before sending to the API, +// so { and } in these should NOT be escaped. But the actual text in these should be. + +public static class Locale { /// - /// Class that holds all customizable string the bot may reply with. - /// Also, descriptions of error codes. - /// Not ideal, but slightly better (is it?) than holding these in appsettings.json. + /// Template for reply for /about command. {0} is assembly version. /// - - // Please note that these should be formatted as Telegram-flavoured Markdown - // see https://core.telegram.org/bots/api#markdownv2-style - // Templating parameters (like {0}) are filled in before sending to the API, - // so { and } in these should NOT be escaped. But the actual text in these should be. - - public static class Locale - { - /// - /// Template for reply for /about command. {0} is assembly version. - /// - public const string About = """ + public const string About = """ **Dotnet Telegram forwarder** v {0}\. [Open\-source\!](https://github.com/bnfour/dotnet-telegram-forwarder) by bnfour, 2018, 2020\–2024\. """; - /// - /// Message to show when token deletion was cancelled. - /// - public const string CancelDeletion = """ + /// + /// Message to show when token deletion was cancelled. + /// + public const string CancelDeletion = """ Token deletion cancelled\. """; - - /// - /// Message to show when token regeneration was cancelled. - /// - public const string CancelRegeneration = """ + + /// + /// Message to show when token regeneration was cancelled. + /// + public const string CancelRegeneration = """ Token regeneration cancelled\. """; - /// - /// Message to show when token deletion was completed. - /// - public const string ConfirmDeletion = """ + /// + /// Message to show when token deletion was completed. + /// + public const string ConfirmDeletion = """ Token deleted\. Thank you for giving us an oppurtunity to serve you\. """; - /// - /// Template for message to show when token regeneration was completed. - /// {0} is new token. - /// - public const string ConfirmRegeneration = """ + /// + /// Template for message to show when token regeneration was completed. + /// {0} is new token. + /// + public const string ConfirmRegeneration = """ Your new token is `{0}` Don't forget to update your clients' settings\. """; - - /// - /// Message to reply to /create with when registration is disabled. - /// - public const string CreateGoAway = """ + + /// + /// Message to reply to /create with when registration is disabled. + /// + public const string CreateGoAway = """ This instance of the bot is not accepting new users for now\. """; - - /// - /// Template for message to show when token was created successfully. - /// {0} is new token. - /// - public const string CreateSuccess = """ + + /// + /// Template for message to show when token was created successfully. + /// {0} is new token. + /// + public const string CreateSuccess = """ Success\! Your token is: `{0}` Please consult /token for usage\. """; - - /// - /// Message to show when user initiated token deletion and registration is off. - /// - public const string DeletionNoTurningBack = """ + + /// + /// Message to show when user initiated token deletion and registration is off. + /// + public const string DeletionNoTurningBack = """ This bot has registration turned *off*\. You won't be able to create new token\. Please be certain\. """; - /// - /// Message to show when user initiated token deletion. - /// - public const string DeletionPending = """ + /// + /// Message to show when user initiated token deletion. + /// + public const string DeletionPending = """ *Token deletion pending\!* Please either /confirm or /cancel it\. This cannot be undone\. If you need to change your token, consider to /regenerate it instead of deleting and re\-creating\. """; - - /// - /// Message to show when confirmation is pending and user tries to use - /// non-confirming command. - /// - public const string ErrorConfirmationPending = """ + + /// + /// Message to show when confirmation is pending and user tries to use + /// non-confirming command. + /// + public const string ErrorConfirmationPending = """ You have an operation pending confirmation\. Please /confirm or /cancel it before using other commands\. """; - - /// - /// Message to show to unknown commands starting with slash. - /// - public const string ErrorDave = """ + + /// + /// Message to show to unknown commands starting with slash. + /// + public const string ErrorDave = """ I'm afraid I can't let you do that\. """; - - /// - /// Message to show when usage of command requires no token set, - /// but user has one. - /// - public const string ErrorMustBeGuest = """ + + /// + /// Message to show when usage of command requires no token set, + /// but user has one. + /// + public const string ErrorMustBeGuest = """ In order to use this command, you must have no token associated with your account\. You can /delete your existing one, but why? """; - - /// - /// Message to show when usage of command requires a token, but user has none. - /// - public const string ErrorMustBeUser = """ + + /// + /// Message to show when usage of command requires a token, but user has none. + /// + public const string ErrorMustBeUser = """ In order to use this command, you must have a token associated with your account\. /create one\. """; - - /// - /// Message to show when user is trying to use confirming command, but no - /// confirmation is pending. - /// - public const string ErrorNoConfirmationPending = """ + + /// + /// Message to show when user is trying to use confirming command, but no + /// confirmation is pending. + /// + public const string ErrorNoConfirmationPending = """ This command is only useful when you're trying to /delete or /regenerate your token\. """; - - /// - /// Message to show on unknown input. - /// - public const string ErrorWhat = """ + + /// + /// Message to show on unknown input. + /// + public const string ErrorWhat = """ Unfortunately, I'm not sure how to interpret this\. """; - - /// - /// Output of /help command. - /// - public const string Help = """ + + /// + /// Output of /help command. + /// + public const string Help = """ This app provides a web API to route messages from API's endpoint to you in Telegram via this bot\. It can be used to notify you in Telegram from any Internet\-enabled device you want \(provided you know how to make POST requests from it\)\. To start, /create your token\. You can /delete it anytime\. To change your token for whatever reason, please /regenerate it and not delete and re\-create\. Once you have a token, see /token for additional usage help\. @@ -154,58 +154,58 @@ This command is only useful when you're trying to /delete or /regenerate your to \-\-bnfour """; - - /// - /// Message to show when user initiated token regeneration. - /// - public const string RegenerationPending = """ + + /// + /// Message to show when user initiated token regeneration. + /// + public const string RegenerationPending = """ *Token regenration pending\!* Please either /confirm or /cancel it\. It cannot be undone\. Please be certain\. """; - - /// - /// Message to warn new users that registration is closed. - /// - public const string StartGoAway = """ + + /// + /// Message to warn new users that registration is closed. + /// + public const string StartGoAway = """ Sorry, this instance of bot is not accepting new users for now\. """; - - /// - /// Message to show when user first engages the bot. - /// - public const string StartMessage = """ + + /// + /// Message to show when user first engages the bot. + /// + public const string StartMessage = """ Hello\! This bot provides a standalone web API to relay messages \(text or stickers\) from anything that can send web requests to your Telegram inbox as messages from the bot\. *Please note*: this requires some external tools and knowledge\. If you consider "Send a POST request" a magic gibberish you don't understand, this bot probably isn't much of use to you\. """; - - /// - /// Message to encourage new users to register. - /// - public const string StartRegistrationHint = """ + + /// + /// Message to encourage new users to register. + /// + public const string StartRegistrationHint = """ If that does not stop you, feel free to /create your very own token\. """; - /// - /// Reply to a sticker message with its ID to use with the web API. - /// {0} is sticker's FileId - /// - public const string StickerId = """ + /// + /// Reply to a sticker message with its ID to use with the web API. + /// {0} is sticker's FileId + /// + public const string StickerId = """ This sticker's FileId is `{0}` """; - /// - /// Template for message to reply to /token command with. - /// {0} is token, {1} is API endpoint URL, {2} is vanity quote, - /// {3} is vanity sticker id. - /// - // double braces are escaping for formatting - public const string TokenTemplate = """ + /// + /// Template for message to reply to /token command with. + /// {0} is token, {1} is API endpoint URL, {2} is vanity quote, + /// {3} is vanity sticker id. + /// + // double braces are escaping for formatting + public const string TokenTemplate = """ Your token is `{0}` @@ -228,5 +228,4 @@ Your token is ``` If everything is okay, the API will return a blank 200 OK response\. If something is not okay, a different status code will be returned\. Consult [the documentation](https://github.com/bnfour/dotnet-telegram-forwarder#response) to see error code list\. """; - } } From ce09b7ff33aa9a8eb75101ebd57b350331adf778 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Tue, 30 Apr 2024 15:38:24 +0700 Subject: [PATCH 18/37] moved services configuration from separate Startup into Program; removed setting listening port via arguments --- WebToTelegramCore/Program.cs | 47 ++++++++++++++++-------------- WebToTelegramCore/Startup.cs | 36 ----------------------- WebToTelegramCore/appsettings.json | 1 + 3 files changed, 26 insertions(+), 58 deletions(-) delete mode 100644 WebToTelegramCore/Startup.cs diff --git a/WebToTelegramCore/Program.cs b/WebToTelegramCore/Program.cs index ca4a811..a9202d9 100644 --- a/WebToTelegramCore/Program.cs +++ b/WebToTelegramCore/Program.cs @@ -1,23 +1,18 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using System; -using Bnfour.WebToTelegramCore.Interfaces; +using System; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Services; +using Bnfour.WebToTelegramCore.Options; namespace Bnfour.WebToTelegramCore; public class Program { - public static void Main(string[] args) + public static void Main() { - // hardcoded default, because 8080 and 8081 were already taken on my machine - int port = 8082; - - if (args.Length == 2 && args[0].Equals("--port") && int.TryParse(args[1], out int nonDefPort)) - { - port = nonDefPort; - } - var builder = WebApplication.CreateBuilder(new WebApplicationOptions { // quick fix to prevent using solution folder for configuration files @@ -32,16 +27,24 @@ public static void Main(string[] args) // file name goes straight after |DataDirectory|, no slashes of any kind AppDomain.CurrentDomain.SetData("DataDirectory", AppContext.BaseDirectory); - builder.WebHost.UseKestrel(kestrelOptions => - { - // this won't work without a SSL proxy over it anyway, - // so listening to localhost only - kestrelOptions.ListenLocalhost(port); - }); + builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")), + ServiceLifetime.Scoped); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddTransient(); + + // Telegram.Bot's Update class relies on some Newtonsoft attributes, + // so to deserialize it correctly, we need to use this library as well + builder.Services.AddControllers().AddNewtonsoftJson(); - // carrying over legacy startup-based configuration (for now? even in 2024?) - var startup = new Startup(builder.Configuration); - startup.ConfigureServices(builder.Services); + builder.Services.Configure(builder.Configuration.GetSection("General")); + builder.Services.Configure(builder.Configuration.GetSection("Bandwidth")); var app = builder.Build(); app.MapControllers(); diff --git a/WebToTelegramCore/Startup.cs b/WebToTelegramCore/Startup.cs deleted file mode 100644 index fd8991c..0000000 --- a/WebToTelegramCore/Startup.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Bnfour.WebToTelegramCore.Interfaces; -using Bnfour.WebToTelegramCore.Options; -using Bnfour.WebToTelegramCore.Services; - -namespace Bnfour.WebToTelegramCore; - -public class Startup(IConfiguration configuration) -{ - public IConfiguration Configuration { get; } = configuration; - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddDbContext(options => - options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")), - ServiceLifetime.Scoped); - - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - services.AddScoped(); - - services.AddTransient(); - - // Telegram.Bot's Update class relies on some Newtonsoft attributes, - // so to deserialize it correctly, we need to use this library as well - services.AddControllers().AddNewtonsoftJson(); - - services.Configure(Configuration.GetSection("General")); - services.Configure(Configuration.GetSection("Bandwidth")); - } -} diff --git a/WebToTelegramCore/appsettings.json b/WebToTelegramCore/appsettings.json index c74251b..3e961e0 100644 --- a/WebToTelegramCore/appsettings.json +++ b/WebToTelegramCore/appsettings.json @@ -5,6 +5,7 @@ } }, "AllowedHosts": "*", + "Urls": "http://localhost:8082", "ConnectionStrings": { "DefaultConnection": "Data Source=|DataDirectory|database.sqlite" }, From a2fb9f0aa294ead3d9488042f11cb7839094ba6d Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 2 May 2024 15:29:26 +0700 Subject: [PATCH 19/37] a new option entry to control upcoming creation commands it's off by default because i set up those manually long ago and i can't be bothered to clean up debug bot's state every time i run this one locally --- WebToTelegramCore/Options/CommonOptions.cs | 5 +++++ WebToTelegramCore/appsettings.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/WebToTelegramCore/Options/CommonOptions.cs b/WebToTelegramCore/Options/CommonOptions.cs index 53e5d8d..33bff12 100644 --- a/WebToTelegramCore/Options/CommonOptions.cs +++ b/WebToTelegramCore/Options/CommonOptions.cs @@ -20,4 +20,9 @@ public class CommonOptions /// Indicates whether new users without a token can use /create command. /// public bool RegistrationEnabled { get; set; } + + /// + /// Indicates whether to create menu entries for bot commands for Telegram clients. + /// + public bool FillCommands { get; set; } } diff --git a/WebToTelegramCore/appsettings.json b/WebToTelegramCore/appsettings.json index 3e961e0..042b89d 100644 --- a/WebToTelegramCore/appsettings.json +++ b/WebToTelegramCore/appsettings.json @@ -12,7 +12,8 @@ "General": { "Token": "it's a secret", "ApiEndpointUrl": "also a secret for now", - "RegistrationEnabled": true + "RegistrationEnabled": true, + "FillCommands": false }, "Bandwidth": { "MaximumCount": 20, From 32d251c34f431fff152775932e3bbb24956cf0ee Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 2 May 2024 15:47:40 +0700 Subject: [PATCH 20/37] descriptions for most of the commands mostly taken directly from the active instance where those were set manually long ago --- WebToTelegramCore/BotCommands/AboutCommand.cs | 2 ++ WebToTelegramCore/BotCommands/BotCommandBase.cs | 6 ++++++ WebToTelegramCore/BotCommands/CancelCommand.cs | 2 ++ WebToTelegramCore/BotCommands/ConfirmCommand.cs | 2 ++ .../BotCommands/ConfirmationCommandBase.cs | 5 +++++ WebToTelegramCore/BotCommands/CreateCommand.cs | 2 ++ WebToTelegramCore/BotCommands/DeleteCommand.cs | 2 ++ WebToTelegramCore/BotCommands/DirectiveCommand.cs | 3 +++ WebToTelegramCore/BotCommands/HelpCommand.cs | 2 ++ .../BotCommands/RegenerateCommand.cs | 2 ++ WebToTelegramCore/BotCommands/StartCommand.cs | 3 +++ WebToTelegramCore/BotCommands/TokenCommand.cs | 2 ++ WebToTelegramCore/Interfaces/IBotCommand.cs | 6 ++++++ WebToTelegramCore/Resources/Locale.cs | 15 +++++++++++++++ 14 files changed, 54 insertions(+) diff --git a/WebToTelegramCore/BotCommands/AboutCommand.cs b/WebToTelegramCore/BotCommands/AboutCommand.cs index 10eb6e2..40f24f5 100644 --- a/WebToTelegramCore/BotCommands/AboutCommand.cs +++ b/WebToTelegramCore/BotCommands/AboutCommand.cs @@ -15,6 +15,8 @@ public class AboutCommand : BotCommandBase, IBotCommand /// public override string Command => "/about"; + public override string Description => Locale.AboutDescription; + /// /// Constructor. /// diff --git a/WebToTelegramCore/BotCommands/BotCommandBase.cs b/WebToTelegramCore/BotCommands/BotCommandBase.cs index 53d8fe3..f39e346 100644 --- a/WebToTelegramCore/BotCommands/BotCommandBase.cs +++ b/WebToTelegramCore/BotCommands/BotCommandBase.cs @@ -16,6 +16,12 @@ public abstract class BotCommandBase : IBotCommand /// public abstract string Command { get; } + /// + /// Short description to be shown alongside the command text in the bot menu + /// within the clients. If empty, the command will not be shown. + /// + public abstract string Description { get; } + /// /// Constructor. /// diff --git a/WebToTelegramCore/BotCommands/CancelCommand.cs b/WebToTelegramCore/BotCommands/CancelCommand.cs index 5a51a31..0569541 100644 --- a/WebToTelegramCore/BotCommands/CancelCommand.cs +++ b/WebToTelegramCore/BotCommands/CancelCommand.cs @@ -16,6 +16,8 @@ public class CancelCommand : ConfirmationCommandBase, IBotCommand /// public override string Command => "/cancel"; + public override string Description => Locale.CancelDescription; + /// /// Constructor. /// diff --git a/WebToTelegramCore/BotCommands/ConfirmCommand.cs b/WebToTelegramCore/BotCommands/ConfirmCommand.cs index 2d3d8b6..5894549 100644 --- a/WebToTelegramCore/BotCommands/ConfirmCommand.cs +++ b/WebToTelegramCore/BotCommands/ConfirmCommand.cs @@ -16,6 +16,8 @@ public class ConfirmCommand : ConfirmationCommandBase, IBotCommand /// public override string Command => "/confirm"; + public override string Description => Locale.ConfirmDescription; + /// /// Database context reference to perform DB operations. /// diff --git a/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs b/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs index d3cdfc0..28be308 100644 --- a/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs +++ b/WebToTelegramCore/BotCommands/ConfirmationCommandBase.cs @@ -17,6 +17,11 @@ public abstract class ConfirmationCommandBase : IBotCommand /// public abstract string Command { get; } + /// + /// Command description for the menu; + /// + public abstract string Description { get; } + /// /// Constructor. /// diff --git a/WebToTelegramCore/BotCommands/CreateCommand.cs b/WebToTelegramCore/BotCommands/CreateCommand.cs index d2488ac..42e30d7 100644 --- a/WebToTelegramCore/BotCommands/CreateCommand.cs +++ b/WebToTelegramCore/BotCommands/CreateCommand.cs @@ -14,6 +14,8 @@ public class CreateCommand : GuestOnlyCommandBase, IBotCommand /// public override string Command => "/create"; + public override string Description => Locale.CreateDescription; + /// /// Field to store database context reference. /// diff --git a/WebToTelegramCore/BotCommands/DeleteCommand.cs b/WebToTelegramCore/BotCommands/DeleteCommand.cs index 3025037..9d4195e 100644 --- a/WebToTelegramCore/BotCommands/DeleteCommand.cs +++ b/WebToTelegramCore/BotCommands/DeleteCommand.cs @@ -16,6 +16,8 @@ public class DeleteCommand : UserOnlyCommandBase, IBotCommand /// public override string Command => "/delete"; + public override string Description => Locale.DeleteDescription; + /// /// Boolean that indicates whether this instance is accepting new users. /// True if it does. diff --git a/WebToTelegramCore/BotCommands/DirectiveCommand.cs b/WebToTelegramCore/BotCommands/DirectiveCommand.cs index f73f1d0..e5c7195 100644 --- a/WebToTelegramCore/BotCommands/DirectiveCommand.cs +++ b/WebToTelegramCore/BotCommands/DirectiveCommand.cs @@ -20,6 +20,9 @@ public class DirectiveCommand : BotCommandBase, IBotCommand /// public override string Command => "/directive"; + // this command does not show up in the menu + public override string Description => string.Empty; + /// /// Constructor that does literally nothing yet required due to my "superb" /// planning skills. diff --git a/WebToTelegramCore/BotCommands/HelpCommand.cs b/WebToTelegramCore/BotCommands/HelpCommand.cs index 1f3e118..074aef7 100644 --- a/WebToTelegramCore/BotCommands/HelpCommand.cs +++ b/WebToTelegramCore/BotCommands/HelpCommand.cs @@ -14,6 +14,8 @@ public class HelpCommand : BotCommandBase, IBotCommand /// public override string Command => "/help"; + public override string Description => Locale.HelpDescription; + /// /// Constructor. /// diff --git a/WebToTelegramCore/BotCommands/RegenerateCommand.cs b/WebToTelegramCore/BotCommands/RegenerateCommand.cs index f2e7105..0a661c9 100644 --- a/WebToTelegramCore/BotCommands/RegenerateCommand.cs +++ b/WebToTelegramCore/BotCommands/RegenerateCommand.cs @@ -16,6 +16,8 @@ public class RegenerateCommand : UserOnlyCommandBase, IBotCommand /// public override string Command => "/regenerate"; + public override string Description => Locale.RegenerateDescription; + /// /// Constructor. /// diff --git a/WebToTelegramCore/BotCommands/StartCommand.cs b/WebToTelegramCore/BotCommands/StartCommand.cs index 68b29fc..e9c679f 100644 --- a/WebToTelegramCore/BotCommands/StartCommand.cs +++ b/WebToTelegramCore/BotCommands/StartCommand.cs @@ -19,6 +19,9 @@ public class StartCommand : BotCommandBase, IBotCommand /// public override string Command => "/start"; + // does not show up in the menu + public override string Description => string.Empty; + /// /// Constructor. /// diff --git a/WebToTelegramCore/BotCommands/TokenCommand.cs b/WebToTelegramCore/BotCommands/TokenCommand.cs index c4827f6..aede493 100644 --- a/WebToTelegramCore/BotCommands/TokenCommand.cs +++ b/WebToTelegramCore/BotCommands/TokenCommand.cs @@ -16,6 +16,8 @@ public class TokenCommand : UserOnlyCommandBase, IBotCommand /// public override string Command => "/token"; + public override string Description => Locale.TokenDescription; + /// /// Random quotes to display as message example. /// diff --git a/WebToTelegramCore/Interfaces/IBotCommand.cs b/WebToTelegramCore/Interfaces/IBotCommand.cs index 85a497b..f07301a 100644 --- a/WebToTelegramCore/Interfaces/IBotCommand.cs +++ b/WebToTelegramCore/Interfaces/IBotCommand.cs @@ -14,6 +14,12 @@ public interface IBotCommand /// string Command { get; } + /// + /// Short description to be shown alongside the command text in the bot menu + /// within the clients. If empty, the command will not be shown. + /// + string Description { get; } + /// /// Method to process received message. /// diff --git a/WebToTelegramCore/Resources/Locale.cs b/WebToTelegramCore/Resources/Locale.cs index eb1d46c..33700f9 100644 --- a/WebToTelegramCore/Resources/Locale.cs +++ b/WebToTelegramCore/Resources/Locale.cs @@ -228,4 +228,19 @@ Your token is ``` If everything is okay, the API will return a blank 200 OK response\. If something is not okay, a different status code will be returned\. Consult [the documentation](https://github.com/bnfour/dotnet-telegram-forwarder#response) to see error code list\. """; + + #region command descriptions + + public const string AboutDescription = "general bot info"; + public const string CancelDescription = "cancel pending deletion or regeneration, if any"; + public const string ConfirmDescription = "confirm pending deletion or regeneration, if any"; + public const string CreateDescription = "create a new token for account"; + public const string DeleteDescription = "delete existing token"; + // Directive is unlisted + public const string HelpDescription = "slightly more verbose help"; + public const string RegenerateDescription = "replace existing token"; + // Start is unlisted + public const string TokenDescription = "your token and usage help"; + + #endregion } From 791bcbfa244de4937bda7f395e57c0068eaf5fa7 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 2 May 2024 16:00:44 +0700 Subject: [PATCH 21/37] a new service to hold the list of commands (because they are now going to be used not only in the handler service, but also in another one to create the menu) just outlined, no implementation, not plugged in to anything --- .../Interfaces/IBotCommandFactory.cs | 16 ++++++++++++++++ WebToTelegramCore/Program.cs | 2 ++ .../Services/BotCommandFactoryService.cs | 10 ++++++++++ WebToTelegramCore/Services/TelegramBotService.cs | 2 +- 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 WebToTelegramCore/Interfaces/IBotCommandFactory.cs create mode 100644 WebToTelegramCore/Services/BotCommandFactoryService.cs diff --git a/WebToTelegramCore/Interfaces/IBotCommandFactory.cs b/WebToTelegramCore/Interfaces/IBotCommandFactory.cs new file mode 100644 index 0000000..9f343f0 --- /dev/null +++ b/WebToTelegramCore/Interfaces/IBotCommandFactory.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Bnfour.WebToTelegramCore.BotCommands; + +namespace Bnfour.WebToTelegramCore.Interfaces; + +/// +/// Provides the list of commands availiable for the bot. +/// +public interface IBotCommandFactory +{ + /// + /// The list of commands available. + /// Should be ordered by the menu orded. + /// + IEnumerable Commands { get; } +} diff --git a/WebToTelegramCore/Program.cs b/WebToTelegramCore/Program.cs index a9202d9..976be9f 100644 --- a/WebToTelegramCore/Program.cs +++ b/WebToTelegramCore/Program.cs @@ -31,6 +31,8 @@ public static void Main() options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Scoped); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/WebToTelegramCore/Services/BotCommandFactoryService.cs b/WebToTelegramCore/Services/BotCommandFactoryService.cs new file mode 100644 index 0000000..051bcc3 --- /dev/null +++ b/WebToTelegramCore/Services/BotCommandFactoryService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Bnfour.WebToTelegramCore.BotCommands; +using Bnfour.WebToTelegramCore.Interfaces; + +namespace Bnfour.WebToTelegramCore.Services; + +public class BotCommandFactoryService : IBotCommandFactory +{ + public IEnumerable Commands => throw new System.NotImplementedException("soon™"); +} diff --git a/WebToTelegramCore/Services/TelegramBotService.cs b/WebToTelegramCore/Services/TelegramBotService.cs index 5da4cd2..b65983b 100644 --- a/WebToTelegramCore/Services/TelegramBotService.cs +++ b/WebToTelegramCore/Services/TelegramBotService.cs @@ -47,7 +47,7 @@ public TelegramBotService(IOptions options) /// public async Task SetExternalWebhook() { - await _client.SetWebhookAsync(_webhookUrl, allowedUpdates: new[] { UpdateType.Message }); + await _client.SetWebhookAsync(_webhookUrl, allowedUpdates: [UpdateType.Message]); } /// From 5f0ecfd5a8fbd642933007371b9744764e831256 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 2 May 2024 16:16:00 +0700 Subject: [PATCH 22/37] bot command factory: fixed return type, implemented the factory itself --- .../Interfaces/IBotCommandFactory.cs | 2 +- .../Services/BotCommandFactoryService.cs | 32 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/WebToTelegramCore/Interfaces/IBotCommandFactory.cs b/WebToTelegramCore/Interfaces/IBotCommandFactory.cs index 9f343f0..b58da2d 100644 --- a/WebToTelegramCore/Interfaces/IBotCommandFactory.cs +++ b/WebToTelegramCore/Interfaces/IBotCommandFactory.cs @@ -12,5 +12,5 @@ public interface IBotCommandFactory /// The list of commands available. /// Should be ordered by the menu orded. /// - IEnumerable Commands { get; } + IEnumerable Commands { get; } } diff --git a/WebToTelegramCore/Services/BotCommandFactoryService.cs b/WebToTelegramCore/Services/BotCommandFactoryService.cs index 051bcc3..3265bb8 100644 --- a/WebToTelegramCore/Services/BotCommandFactoryService.cs +++ b/WebToTelegramCore/Services/BotCommandFactoryService.cs @@ -1,10 +1,38 @@ using System.Collections.Generic; using Bnfour.WebToTelegramCore.BotCommands; using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Options; +using Microsoft.Extensions.Options; namespace Bnfour.WebToTelegramCore.Services; -public class BotCommandFactoryService : IBotCommandFactory +/// +/// Provides the list of commands available. +/// +/// Options to pass to the commands. +/// Context used by some of the commands. +/// Service used by some of the commands. +public class BotCommandFactoryService(IOptions options, RecordContext context, ITokenGeneratorService generator) : IBotCommandFactory { - public IEnumerable Commands => throw new System.NotImplementedException("soon™"); + // TODO it's not necessary to instantiate all the commands in TelegramApiService + // when we know the command text + // consider providing a method that searches a command by its text + + // ordered by the order of menu appearance; /directive and /start are not shown + // and technically can be placed whereever + public IEnumerable Commands => + [ + + new TokenCommand(options.Value.ApiEndpointUrl), + new CreateCommand(context, generator, options.Value.RegistrationEnabled), + new DeleteCommand(options.Value.RegistrationEnabled), + new RegenerateCommand(), + new ConfirmCommand(context, generator), + new CancelCommand(), + new HelpCommand(), + new AboutCommand(), + + new StartCommand(options.Value.RegistrationEnabled), + new DirectiveCommand() + ]; } From 310e05833ce84a0af1ee3101ca3bd9bdd3b721e1 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 2 May 2024 16:24:47 +0700 Subject: [PATCH 23/37] TelegramApiService: now uses the new command factory instead of the built-in list also i finally understand the primary constructor syntax sugar -- it's cool! (but not cool enough for me to replace _all_ older code with it -- i'm changing as i do other things to classes) --- .../Services/TelegramApiService.cs | 80 ++++--------------- 1 file changed, 17 insertions(+), 63 deletions(-) diff --git a/WebToTelegramCore/Services/TelegramApiService.cs b/WebToTelegramCore/Services/TelegramApiService.cs index 802ea4f..1048cc1 100644 --- a/WebToTelegramCore/Services/TelegramApiService.cs +++ b/WebToTelegramCore/Services/TelegramApiService.cs @@ -1,12 +1,10 @@ using Microsoft.Extensions.Options; using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -using Bnfour.WebToTelegramCore.BotCommands; using Bnfour.WebToTelegramCore.Interfaces; using Bnfour.WebToTelegramCore.Models; using Bnfour.WebToTelegramCore.Options; @@ -17,65 +15,21 @@ namespace Bnfour.WebToTelegramCore.Services; /// /// Class that implements handling webhook updates. /// -public class TelegramApiService : ITelegramApiService +/// +/// Constructor that injects dependencies and configures list of commands. +/// +/// Options that include token. +/// Database context to use. +/// Bot service instance to use. +/// Command list provider. +public class TelegramApiService(IOptions options, + RecordContext context, ITelegramMessageService bot, + IBotCommandFactory commandFactory) : ITelegramApiService { /// /// Bot's token. Used to verify update origin. /// - private readonly string _token; - - /// - /// Database context to use. - /// - private readonly RecordContext _context; - - /// - /// Bot service to send messages (and sometimes stickers). - /// - private readonly ITelegramMessageService _bot; - - /// - /// Reference to token generator service. - /// - private readonly ITokenGeneratorService _generator; - - /// - /// List of commands available to the bot. - /// - private readonly List _commands; - - /// - /// Constructor that injects dependencies and configures list of commands. - /// - /// Options that include token. - /// Database context to use. - /// Bot service instance to use. - /// Token generator service to use. - public TelegramApiService(IOptions options, - RecordContext context, ITelegramMessageService bot, - ITokenGeneratorService generator) - { - _token = options.Value.Token; - _context = context; - _bot = bot; - _generator = generator; - - var isRegistrationOpen = options.Value.RegistrationEnabled; - - _commands = - [ - new StartCommand(isRegistrationOpen), - new TokenCommand(options.Value.ApiEndpointUrl), - new RegenerateCommand(), - new DeleteCommand(isRegistrationOpen), - new ConfirmCommand(_context, _generator), - new CancelCommand(), - new HelpCommand(), - new DirectiveCommand(), - new AboutCommand(), - new CreateCommand(_context, _generator, isRegistrationOpen) - ]; - } + private readonly string _token = options.Value.Token; /// /// Method to handle incoming updates from the webhook. @@ -124,17 +78,17 @@ private async Task HandleTextMessage(Update update) } // if user has no record associated, make him a mock one with just an account number, // so we know who they are in case we're going to create them a proper one - Record record = await _context.GetRecordByAccountId(userId.Value) + Record record = await context.GetRecordByAccountId(userId.Value) ?? new Record(null, userId.Value); IBotCommand handler = null; string commandText = text.Split(' ').FirstOrDefault(); // will crash if multiple command classes share same text, who cares - handler = _commands.SingleOrDefault(c => c.Command.Equals(commandText)); + handler = commandFactory.Commands.SingleOrDefault(c => c.Command.Equals(commandText)); if (handler != null) { - await _bot.Send(userId.Value, handler.Process(record)); + await bot.Send(userId.Value, handler.Process(record)); } else { @@ -154,14 +108,14 @@ private async Task HandleUnknownText(long accountId, string text) // suddenly, cat! if (new Random().Next(0, 19) == 0) { - await _bot.SendTheSticker(accountId); + await bot.SendTheSticker(accountId); } else { string reply = text.StartsWith('/') ? Locale.ErrorDave : Locale.ErrorWhat; - await _bot.Send(accountId, reply); + await bot.Send(accountId, reply); } } @@ -180,7 +134,7 @@ private async Task HandleSticker(Update update) return; } var message = string.Format(Locale.StickerId, fileId); - await _bot.Send(userId.Value, message); + await bot.Send(userId.Value, message); } /// From 9a5193c73c1fbb1a1f0c1de0de780aa5b2edd7ac Mon Sep 17 00:00:00 2001 From: Bn4 Date: Thu, 2 May 2024 19:10:37 +0700 Subject: [PATCH 24/37] interfaces for the upcoming services for command setting --- .../Interfaces/ICommandManagerService.cs | 14 +++++++++++ .../Interfaces/ITelegramCommandsService.cs | 23 +++++++++++++++++++ .../Services/BotCommandFactoryService.cs | 1 - 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 WebToTelegramCore/Interfaces/ICommandManagerService.cs create mode 100644 WebToTelegramCore/Interfaces/ITelegramCommandsService.cs diff --git a/WebToTelegramCore/Interfaces/ICommandManagerService.cs b/WebToTelegramCore/Interfaces/ICommandManagerService.cs new file mode 100644 index 0000000..d38840c --- /dev/null +++ b/WebToTelegramCore/Interfaces/ICommandManagerService.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace Bnfour.WebToTelegramCore.Interfaces; + +/// +/// Used to (re-)create the list of commands seen in Telegram clients on startup. +/// +public interface ICommandManagerService +{ + /// + /// Checks the app settings, and if configured, drops and recreates the commands list. + /// + Task SetCommandsIfNeeded(); +} diff --git a/WebToTelegramCore/Interfaces/ITelegramCommandsService.cs b/WebToTelegramCore/Interfaces/ITelegramCommandsService.cs new file mode 100644 index 0000000..f564600 --- /dev/null +++ b/WebToTelegramCore/Interfaces/ITelegramCommandsService.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bnfour.WebToTelegramCore.Interfaces; + +/// +/// Interface to manage bot's commands (known as "my commands" in API docs). +/// Very simple -- default scope, no localization. +/// +public interface ITelegramCommandsService +{ + /// + /// Deletes any set commands. + /// + Task DeleteCommands(); + + /// + /// Sets the new list of commands for the bot. + /// + /// List of commands to set; + /// represented as ready-to-send entities. + Task SetCommands(IEnumerable commands); +} diff --git a/WebToTelegramCore/Services/BotCommandFactoryService.cs b/WebToTelegramCore/Services/BotCommandFactoryService.cs index 3265bb8..aa0345c 100644 --- a/WebToTelegramCore/Services/BotCommandFactoryService.cs +++ b/WebToTelegramCore/Services/BotCommandFactoryService.cs @@ -22,7 +22,6 @@ public class BotCommandFactoryService(IOptions options, RecordCon // and technically can be placed whereever public IEnumerable Commands => [ - new TokenCommand(options.Value.ApiEndpointUrl), new CreateCommand(context, generator, options.Value.RegistrationEnabled), new DeleteCommand(options.Value.RegistrationEnabled), From fca8ce7ae7864c13a44c182e304b2d4066bc78f4 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Fri, 3 May 2024 01:07:57 +0700 Subject: [PATCH 25/37] TelegramBotService: implemented ITelegramCommandsService it's basically a thin wrapper around Telegram.Bot's API; actual setting will be in ICommandManagerService implementation (i'm still unsure about the three interfaces in single class -- in particular, command manager can implement _client to interact with API itself) --- WebToTelegramCore/Program.cs | 1 + WebToTelegramCore/Services/TelegramBotService.cs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/WebToTelegramCore/Program.cs b/WebToTelegramCore/Program.cs index 976be9f..adafba2 100644 --- a/WebToTelegramCore/Program.cs +++ b/WebToTelegramCore/Program.cs @@ -35,6 +35,7 @@ public static void Main() builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/WebToTelegramCore/Services/TelegramBotService.cs b/WebToTelegramCore/Services/TelegramBotService.cs index b65983b..30c8282 100644 --- a/WebToTelegramCore/Services/TelegramBotService.cs +++ b/WebToTelegramCore/Services/TelegramBotService.cs @@ -6,6 +6,7 @@ using Bnfour.WebToTelegramCore.Data; using Bnfour.WebToTelegramCore.Interfaces; using Bnfour.WebToTelegramCore.Options; +using System.Collections.Generic; namespace Bnfour.WebToTelegramCore.Services; @@ -13,7 +14,8 @@ namespace Bnfour.WebToTelegramCore.Services; /// Provides realization to Telegram bot related service interfaces. /// Encapsulates Telegram.Bot API to send messages and manage webhooks. /// -public class TelegramBotService : ITelegramMessageService, ITelegramWebhookService +public class TelegramBotService : ITelegramMessageService, + ITelegramWebhookService, ITelegramCommandsService { /// /// Field to store bot client instance. @@ -90,6 +92,16 @@ public async Task SendSticker(long accountId, string stickerFileId, bool silent await _client.SendStickerAsync(chatId, sticker, disableNotification: silent); } + public async Task DeleteCommands() + { + await _client.DeleteMyCommandsAsync(); + } + + public async Task SetCommands(IEnumerable commands) + { + await _client.SetMyCommandsAsync(commands); + } + /// /// Method to send predefined sticker on behalf of the bot. /// From fb560951e7861052496061cd79528a827a99f834 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Fri, 3 May 2024 01:47:53 +0700 Subject: [PATCH 26/37] CommandManagerService: maybe implemented (not tested yet, unsure of formatting), plugged into DI --- WebToTelegramCore/Program.cs | 9 +++- .../Services/CommandManagerService.cs | 45 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 WebToTelegramCore/Services/CommandManagerService.cs diff --git a/WebToTelegramCore/Program.cs b/WebToTelegramCore/Program.cs index adafba2..5ebba75 100644 --- a/WebToTelegramCore/Program.cs +++ b/WebToTelegramCore/Program.cs @@ -37,6 +37,8 @@ public static void Main() builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -56,8 +58,11 @@ public static void Main() { using (var scope = app.Services.CreateScope()) { - var service = scope.ServiceProvider.GetService(); - await service.SetExternalWebhook(); + var webhookService = scope.ServiceProvider.GetService(); + await webhookService.SetExternalWebhook(); + + var commandService = scope.ServiceProvider.GetService(); + await commandService.SetCommandsIfNeeded(); } }); diff --git a/WebToTelegramCore/Services/CommandManagerService.cs b/WebToTelegramCore/Services/CommandManagerService.cs new file mode 100644 index 0000000..91da08c --- /dev/null +++ b/WebToTelegramCore/Services/CommandManagerService.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bnfour.WebToTelegramCore.Interfaces; +using Bnfour.WebToTelegramCore.Options; +using Microsoft.Extensions.Options; + +namespace Bnfour.WebToTelegramCore.Services; + +/// +/// Class that manages bot's command list (as seen by clients) on app startup. +/// +/// Options: include a parameter to disable this service. +/// Provides a list of all commands available. +/// Used to actually change the command list. +public class CommandManagerService(IOptions options, + IBotCommandFactory factory, ITelegramCommandsService service) + : ICommandManagerService +{ + public async Task SetCommandsIfNeeded() + { + if (!options.Value.FillCommands) + { + return; + } + // drop existing commands to be sure + await service.DeleteCommands(); + + var commandList = new List(); + + foreach (var internalCommand in factory.Commands) + { + if (!string.IsNullOrEmpty(internalCommand.Description)) + { + commandList.Add(new Telegram.Bot.Types.BotCommand + { + // internal command representation starts with a slash + Command = internalCommand.Command.TrimStart('/'), + Description = internalCommand.Description + }); + } + } + + await service.SetCommands(commandList); + } +} From 079c75744a8c0478c498727962f5e015a57b8f3f Mon Sep 17 00:00:00 2001 From: Bn4 Date: Fri, 3 May 2024 13:51:20 +0700 Subject: [PATCH 27/37] readme: include info on command autofill, other small changes --- .../Services/TelegramApiService.cs | 2 +- readme.md | 34 +++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/WebToTelegramCore/Services/TelegramApiService.cs b/WebToTelegramCore/Services/TelegramApiService.cs index 1048cc1..4d5a124 100644 --- a/WebToTelegramCore/Services/TelegramApiService.cs +++ b/WebToTelegramCore/Services/TelegramApiService.cs @@ -98,7 +98,7 @@ private async Task HandleTextMessage(Update update) /// /// Handles unknown text sent to bot. - /// 5% chance of cat sticker, regular text otherwise. + /// About 5% chance of cat sticker, regular text otherwise. /// /// User to reply to. /// Received message that was not processed diff --git a/readme.md b/readme.md index 037b0de..538f4bc 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,5 @@ # Dotnet Telegram forwarder -_(also known as WebToTelegramCore earlier in development)_ +_(also known as WebToTelegramCore internally)_ An app providing HTTP API to deliver arbitrary text or sticker notifications via associated Telegram bot from anywhere where making HTTP POST requests is available. @@ -17,7 +17,7 @@ Request's body has this structure: ```jsonc { "token": "string", - "silent": false, // optional + "silent": false | true, // optional // either these for text notification "message": "string", "type": "plaintext" | "markdown", // optional @@ -26,7 +26,7 @@ Request's body has this structure: } ``` * `token` is this service's user identifier, randomly generated per Telegram user, can be changed and removed by the user. It's a 16 characters long string that may contain alphanumerics, and plus and equals signs (So `[0-9a-zA-Z+=]{16}`). -* `silent` is boolean to indicate whether them message from the bot in Telegram will come with a notification with sound. Behaves what you'd expect. Optional parameter, if not supplied, defaults to `false`. Please note that the end user is able to mute the bot, effectively rendering this option useless. +* `silent` is a boolean to indicate whether them message from the bot in Telegram will come with a notification with sound. Behaves what you'd expect. Optional parameter, if not supplied, defaults to `false`. Please note that the end user is able to mute the bot, effectively rendering this option useless. These two parameters are used for a text notification: * `message` is the text of the message to be sent via the bot. Maximum length is 4096 (also happens to be a maximum length of one Telegram message). @@ -35,7 +35,7 @@ These two parameters are used for a text notification: This parameter should be provided for a sticker notification: * `sticker` is an internal Telegram ID of a sticker. For obtaining values to use, see [Sticker identification](#sticker-identification). -Providing both `message` and `sticker` at the same time is considered an invalid request. When `sticker` is present, `type` is ignored. +Providing both `message` and `sticker` at the same time is considered a bad request. When `sticker` is present, `type` is ignored. #### Response API returns an empty HTTP response with any of the following status codes: @@ -51,7 +51,7 @@ Client should retry later, the included `Retry-After` header suggests the amount Client can try to retry later, but ¯\\\_(ツ)\_/¯ #### Rate limitation -The API has a rate limitation, preventing large amount of notifications in a short amount of time. By default _(can be adjusted via config files),_ every user has _20_ so-called message points. Every notification sent removes 1 message point, and requests will be refused with `429 Too Many Requests` status code when all points are depleted. A single point is regenerated every _minute_ after last message was sent. +The API has a rate limitation, preventing large amount of notifications in a short amount of time. By default _([configurable](#bandwidth-section)),_ every user can send up to _20_ notifications. Every notification sent increases the notifications sent counter by one, and requests will be refused with `429 Too Many Requests` status code when counter is maxed. A single point is subtracted from the counter every _minute_ after last message was sent. For instance, if API is used to send 40 notifications in quick succession, only the 20 first messages will be sent to the user. If the client waits 5 minutes after API starts responding with 429's, they will be able to send 5 more messages before hitting the limit again. After 20 minutes of idle time since the last successfully sent message, the API will behave as usual. @@ -78,13 +78,25 @@ Bot will reply with a message containing the ID of any sticker sent to it. This This works even when there is a pending confirmation which usually blocks most of the commands. ## Configuration and deployment -You'll need .NET 7 runtime. Hosting on GNU/Linux in a proper data center is highly encouraged. -By default, the app listens on port 8082. This can be changed with `--port ` command-line argument. -Rest of the settings are stored inside `appsettings.json`: -* "General" section contains Telegram's bot token, API endpoint URL as seen from outside world (certainly not localhost:8082 as Kestrel would told you it listens to) and a boolean that controls whether new users can create tokens. Please note that `/api` will be appended to this address. So, for example, if you set `https://foo.example.com/bar` here, actual endpoint to be used with the API will be `https://foo.example.com/bar/api`, Telegram webhook endpoint will be `https://foo.example.com/bar/api/{bot-token}` . -* "Bandwidth" section controls bot's [throughput](#rate-limitation): maximum amount of messages to be delivered at once and amount of seconds to regenerate one message delivery is set here. +You'll need .NET 8 runtime. Hosting on GNU/Linux in a proper data center is highly encouraged. -To deploy this bot, you'll need something that will enable SSL as required by Telegram. As always with Telegram bots, I recommend `nginx` as a reverse proxy. +### Configuration +The settings are stored inside `appsettings.json`, app-specific options are defined in "General" and "Bandwidth" sections. + +#### General section +"General" section contains: +- `Token` — bot's Telegram API token; +- `ApiEndpointUrl` — app's endpoint URL as seen from outside world (certainly not localhost:8082 as Kestrel would told you it listens to; SSL wrapper is required); +- `RegistrationEnabled` — a boolean that controls whether new users can create tokens; +- `FillCommands` — a boolean that controls an option to automatically populate the list of bot commands in the menu visible in the dialog with the bot. + +#### Bandwidth section +"Bandwidth" section controls bot's [throughput](#rate-limitation): maximum amount of messages to be delivered at once and amount of seconds to regenerate one message delivery is set here. + +### Deployment +To deploy this bot, you'll need something that will enable SSL as required by Telegram. It is a deliberate choice not to handle it in this app. + +As always with Telegram bots, I recommend `nginx` as a reverse proxy. ## Version history * **v 1.0**, 2018-08-29 From e272bdeb5fb6caaefc0040b5595e49e48c83c84b Mon Sep 17 00:00:00 2001 From: Bn4 Date: Fri, 3 May 2024 13:56:15 +0700 Subject: [PATCH 28/37] configuration: now tolerates ending slash in ApiEndpointUrl --- WebToTelegramCore/Services/TelegramBotService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebToTelegramCore/Services/TelegramBotService.cs b/WebToTelegramCore/Services/TelegramBotService.cs index 30c8282..eefeffc 100644 --- a/WebToTelegramCore/Services/TelegramBotService.cs +++ b/WebToTelegramCore/Services/TelegramBotService.cs @@ -40,7 +40,7 @@ public TelegramBotService(IOptions options) { _client = new TelegramBotClient(options.Value.Token); // made unclear that "api" part is needed as well, shot myself in the leg 3 years after - _webhookUrl = options.Value.ApiEndpointUrl + "/api/" + options.Value.Token; + _webhookUrl = options.Value.ApiEndpointUrl.TrimEnd('/') + "/api/" + options.Value.Token; } /// From 2606618cf5e100268ac482a08845348f8461270e Mon Sep 17 00:00:00 2001 From: Bn4 Date: Tue, 14 May 2024 16:59:57 +0700 Subject: [PATCH 29/37] basic setup for including a browser part of the app: an index page (supposed to contain a minigame later, nothing fancy though) and an epic custom error page (epicness yet to be added) the errors only work for get requests to entertain browser users, the api still returns empty responses --- .../Controllers/ErrorPageController.cs | 21 ++++++++++++ .../Controllers/HomeController.cs | 27 +++++++++++++++ .../Controllers/TelegramApiController.cs | 2 +- .../Controllers/WebApiController.cs | 2 +- WebToTelegramCore/Models/ErrorPageModel.cs | 33 +++++++++++++++++++ WebToTelegramCore/Program.cs | 3 +- .../Views/ErrorPage/OnGet.cshtml | 10 ++++++ WebToTelegramCore/Views/Home/Index.cshtml | 10 ++++++ 8 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 WebToTelegramCore/Controllers/ErrorPageController.cs create mode 100644 WebToTelegramCore/Controllers/HomeController.cs create mode 100644 WebToTelegramCore/Models/ErrorPageModel.cs create mode 100644 WebToTelegramCore/Views/ErrorPage/OnGet.cshtml create mode 100644 WebToTelegramCore/Views/Home/Index.cshtml diff --git a/WebToTelegramCore/Controllers/ErrorPageController.cs b/WebToTelegramCore/Controllers/ErrorPageController.cs new file mode 100644 index 0000000..02f860e --- /dev/null +++ b/WebToTelegramCore/Controllers/ErrorPageController.cs @@ -0,0 +1,21 @@ +using Bnfour.WebToTelegramCore.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Bnfour.WebToTelegramCore.Controllers; + +/// +/// Controller that displays custom error page on GET request errors. +/// (Mostly 404, also 405 if actual API path is requested). +/// +public class ErrorPageController : Controller +{ + // GET /Status/code + // intended to be called internally from UseStatusCodePagesWithReExecute, + // can also be requested directly for a status page + + [HttpGet, Route("/Status/{statusCode}")] + public IActionResult OnGet(int statusCode) + { + return View(new ErrorPageModel(statusCode)); + } +} diff --git a/WebToTelegramCore/Controllers/HomeController.cs b/WebToTelegramCore/Controllers/HomeController.cs new file mode 100644 index 0000000..d0bb0ee --- /dev/null +++ b/WebToTelegramCore/Controllers/HomeController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Bnfour.WebToTelegramCore.Controllers; + +/// +/// Controller for the home and/or landing page. +/// +public class HomeController : Controller +{ + // GET / + + [HttpGet, Route("/")] + public IActionResult Index() + { + return View(); + } + + // GET /favicon.ico + // implemented in a separate method to not include a full-blown error page + // for every request for browsers that do not know that we don't use favicons here + + [SkipStatusCodePages, HttpGet, Route("/favicon.ico")] + public IActionResult Plain404ForFavicon() + { + return NotFound("Imagine using favicons"); + } +} diff --git a/WebToTelegramCore/Controllers/TelegramApiController.cs b/WebToTelegramCore/Controllers/TelegramApiController.cs index d08bf62..e10b577 100644 --- a/WebToTelegramCore/Controllers/TelegramApiController.cs +++ b/WebToTelegramCore/Controllers/TelegramApiController.cs @@ -36,7 +36,7 @@ public TelegramApiController(ITelegramApiService tgApi) /// Update to handle. /// 404 Not Found on wrong tokens, 200 OK otherwise, /// unless there is an internal server error. - [HttpPost, Route("api/{token}")] + [HttpPost, Route("api/{token}"), SkipStatusCodePages] public async Task HandleTelegramApi(string token, [FromBody] Update update) { if (!_tgApi.IsToken(token)) diff --git a/WebToTelegramCore/Controllers/WebApiController.cs b/WebToTelegramCore/Controllers/WebApiController.cs index 2638289..7441631 100644 --- a/WebToTelegramCore/Controllers/WebApiController.cs +++ b/WebToTelegramCore/Controllers/WebApiController.cs @@ -35,7 +35,7 @@ public WebApiController(IOwnApiService ownApi, IOptions option /// Request object in POST request body. /// HTTP status code result indicating whether the request was handled /// successfully, or one of the error codes. - [HttpPost, Route("api")] + [HttpPost, Route("api"), SkipStatusCodePages] public async Task HandleWebApi([FromBody] Request request) { // deny malformed requests as detected by binder, or by "business" logic in here diff --git a/WebToTelegramCore/Models/ErrorPageModel.cs b/WebToTelegramCore/Models/ErrorPageModel.cs new file mode 100644 index 0000000..18efd8c --- /dev/null +++ b/WebToTelegramCore/Models/ErrorPageModel.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace Bnfour.WebToTelegramCore.Models; + +/// +/// Represents the data to be shown on the error page. +/// +/// Status code to be represented. +public class ErrorPageModel(int statusCode) +{ + /// + /// A list of descriptions for supported error codes. + /// + private static readonly Dictionary _codeDescriptions = new() + { + [404] = "Not found", + [405] = "Method not allowed", + [418] = "I'm a teapot", + [500] = "Internal server error", + // an easter egg because i can + [687] = "Well, you found me. Congratulations. Was it worth it?" + }; + + /// + /// Status code for the error page. + /// + public int StatusCode { get; set; } = statusCode; + + /// + /// Human-readable representation of the error. + /// + public string Description => _codeDescriptions.TryGetValue(StatusCode, out string value) ? value : "Unknown error"; +} diff --git a/WebToTelegramCore/Program.cs b/WebToTelegramCore/Program.cs index 5ebba75..e61c290 100644 --- a/WebToTelegramCore/Program.cs +++ b/WebToTelegramCore/Program.cs @@ -46,13 +46,14 @@ public static void Main() // Telegram.Bot's Update class relies on some Newtonsoft attributes, // so to deserialize it correctly, we need to use this library as well - builder.Services.AddControllers().AddNewtonsoftJson(); + builder.Services.AddControllersWithViews().AddNewtonsoftJson(); builder.Services.Configure(builder.Configuration.GetSection("General")); builder.Services.Configure(builder.Configuration.GetSection("Bandwidth")); var app = builder.Build(); app.MapControllers(); + app.UseStatusCodePagesWithReExecute("/Status/{0}"); app.Lifetime.ApplicationStarted.Register(async () => { diff --git a/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml b/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml new file mode 100644 index 0000000..1a68c2b --- /dev/null +++ b/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml @@ -0,0 +1,10 @@ + + + + + @Model.StatusCode @Model.Description + + + @Model.StatusCode @Model.Description + + diff --git a/WebToTelegramCore/Views/Home/Index.cshtml b/WebToTelegramCore/Views/Home/Index.cshtml new file mode 100644 index 0000000..9b693d0 --- /dev/null +++ b/WebToTelegramCore/Views/Home/Index.cshtml @@ -0,0 +1,10 @@ + + + + + soon™ + + + soon™ + + From e7c426ffbad4c6e7e50f44b0b88ec9b969fb2e8f Mon Sep 17 00:00:00 2001 From: Bn4 Date: Tue, 14 May 2024 18:04:09 +0700 Subject: [PATCH 30/37] added epicness to the error page you might have seen it somewhere else, the code is taken from that somewhere else almost verbatim also added empty files for the index page to be implemented later --- WebToTelegramCore/Program.cs | 1 + .../Views/ErrorPage/OnGet.cshtml | 24 ++- .../Views/ErrorPage/_ErrorSvg.cshtml | 199 ++++++++++++++++++ WebToTelegramCore/wwwroot/css/error.css | 51 +++++ WebToTelegramCore/wwwroot/css/home.css | 1 + WebToTelegramCore/wwwroot/css/shared.css | 46 ++++ WebToTelegramCore/wwwroot/js/error.js | 35 +++ WebToTelegramCore/wwwroot/js/game.js | 1 + 8 files changed, 351 insertions(+), 7 deletions(-) create mode 100644 WebToTelegramCore/Views/ErrorPage/_ErrorSvg.cshtml create mode 100644 WebToTelegramCore/wwwroot/css/error.css create mode 100644 WebToTelegramCore/wwwroot/css/home.css create mode 100644 WebToTelegramCore/wwwroot/css/shared.css create mode 100644 WebToTelegramCore/wwwroot/js/error.js create mode 100644 WebToTelegramCore/wwwroot/js/game.js diff --git a/WebToTelegramCore/Program.cs b/WebToTelegramCore/Program.cs index e61c290..b83ffb3 100644 --- a/WebToTelegramCore/Program.cs +++ b/WebToTelegramCore/Program.cs @@ -54,6 +54,7 @@ public static void Main() var app = builder.Build(); app.MapControllers(); app.UseStatusCodePagesWithReExecute("/Status/{0}"); + app.UseStaticFiles(); app.Lifetime.ApplicationStarted.Register(async () => { diff --git a/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml b/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml index 1a68c2b..f1cf4b4 100644 --- a/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml +++ b/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml @@ -1,10 +1,20 @@ - - - @Model.StatusCode @Model.Description - - - @Model.StatusCode @Model.Description - + + + @Model.StatusCode @Model.Description + + + + + + +
+ @await Html.PartialAsync("_ErrorSvg") +

@Model.StatusCode @Model.Description

+ home page +
+ diff --git a/WebToTelegramCore/Views/ErrorPage/_ErrorSvg.cshtml b/WebToTelegramCore/Views/ErrorPage/_ErrorSvg.cshtml new file mode 100644 index 0000000..544a7b1 --- /dev/null +++ b/WebToTelegramCore/Views/ErrorPage/_ErrorSvg.cshtml @@ -0,0 +1,199 @@ +@* +this is not very clean, but still a way to embed svg directly into the page dom, +so my existing js/css from other projects will work here as well +*@ +ЪУЪ diff --git a/WebToTelegramCore/wwwroot/css/error.css b/WebToTelegramCore/wwwroot/css/error.css new file mode 100644 index 0000000..3724b1d --- /dev/null +++ b/WebToTelegramCore/wwwroot/css/error.css @@ -0,0 +1,51 @@ +:root { + --accent-orange: #ca5010; +} + +h1 { + font-family: var(--font-mono); + margin: 16px 0; + text-align: center; +} + +a { + padding: 4px 4px; + color: var(--accent-orange); +} + +a:hover { + background-color: var(--accent-orange); + color: #fff; +} + +.container { + width: 600px; +} + +svg { + max-height: 67vh; +} + +svg #path_for_text { + visibility: hidden; +} + +svg text { + font-family: var(--font-mono) !important; +} + + +/* these numbered ids after the copypaste, i cannot */ + +svg text, svg #body, svg #pupil, svg #pupil-3 { + fill: var(--fg) !important; +} + +svg #eye-white-thing-how-does-it-called-again, +svg #eye-white-thing-how-does-it-called-again-7 { + fill: var(--bg) !important; +} + +svg #all-body > path:not(#body) { + stroke: var(--fg) !important; +} diff --git a/WebToTelegramCore/wwwroot/css/home.css b/WebToTelegramCore/wwwroot/css/home.css new file mode 100644 index 0000000..99b934e --- /dev/null +++ b/WebToTelegramCore/wwwroot/css/home.css @@ -0,0 +1 @@ +/* soon™ */ diff --git a/WebToTelegramCore/wwwroot/css/shared.css b/WebToTelegramCore/wwwroot/css/shared.css new file mode 100644 index 0000000..f8a81ce --- /dev/null +++ b/WebToTelegramCore/wwwroot/css/shared.css @@ -0,0 +1,46 @@ +:root { + --bg: #fff; + --bg-accent: #bbb; + --fg: #000; + + --font-mono: 'Iosevka', 'Consolas', monospace, Courier; + --font-default: 'Helvetica', 'Noto Sans', 'Droid Sans', 'Arial', sans-serif; +} + +* { + box-sizing: border-box; + border: 0 none; + margin: 0; + padding: 0; +} + +html { + width: 100%; +} + +body { + font-family: var(--font-default); + font-size: 1rem; + line-height: 1.1; + + display: flex; + flex-direction: row; + justify-content: center; + + color: var(--fg); + background-color: var(--bg); +} + +.container { + display: flex; + flex-direction: column; + align-items: center; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #333; + --bg-accent: #222; + --fg: #ccc; + } +} diff --git a/WebToTelegramCore/wwwroot/js/error.js b/WebToTelegramCore/wwwroot/js/error.js new file mode 100644 index 0000000..3b8eff0 --- /dev/null +++ b/WebToTelegramCore/wwwroot/js/error.js @@ -0,0 +1,35 @@ +// imagine using raw js in this day and age + +window.addEventListener("load", () => { + // wait a bit before changing + setTimeout(() => do_stuff(), 1000); +}); + +function do_stuff() { + const arr = shuffle([...Array(code.length).keys()]); + const span = document.getElementById("tspan7"); + + for (var i = 0; i < arr.length; i++) + { + // smh my head + (ind => setTimeout(() => replace(span, arr[ind], code), ind * 1200))(i); + } +} + +function replace(element, index, source) { + element.innerHTML = setCharAt(element.innerHTML, index, source[index]); +} + +function shuffle(array) { + let currentIndex = array.length, randomIndex; + while (currentIndex > 0) { + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; + } + return array; +} + +function setCharAt(str, idx, newChr) { + return str.substring(0, idx) + newChr + str.substring(idx + 1); +} diff --git a/WebToTelegramCore/wwwroot/js/game.js b/WebToTelegramCore/wwwroot/js/game.js new file mode 100644 index 0000000..efbdd0c --- /dev/null +++ b/WebToTelegramCore/wwwroot/js/game.js @@ -0,0 +1 @@ +// to be implemented later From 14a545103d1f9cdfb3f8170623effcc4f690436c Mon Sep 17 00:00:00 2001 From: Bn4 Date: Tue, 14 May 2024 19:09:36 +0700 Subject: [PATCH 31/37] readme: some info on web pages to keep the spirit of rdd somewhat alive also a changelog draft and removal of redundant trailing whitespace --- readme.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/readme.md b/readme.md index 538f4bc..5b49999 100644 --- a/readme.md +++ b/readme.md @@ -1,13 +1,13 @@ # Dotnet Telegram forwarder -_(also known as WebToTelegramCore internally)_ +_(also known as WebToTelegramCore internally)_ -An app providing HTTP API to deliver arbitrary text or sticker notifications via associated Telegram bot from anywhere where making HTTP POST requests is available. +An app providing HTTP API to deliver arbitrary text or sticker notifications via associated Telegram bot from anywhere where making HTTP POST requests is available. ## Status -Operational. First version has been serving me flawlessly (as I can tell) since mid 2018. +Operational. In continous service since mid 2018. ## Description -Let's try [readme driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) this time. This app consists of two equally important parts: Telegram bot and a web API. +Let's try [readme driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) this time. This app consists of two equally important parts: [Telegram bot](#telegram-bot) and a [web API](#web-api); and a less important [web page](#web-pages) part. ### Web API Has only one method to send the notification. Its endpoint listens for POST requests with JSON body. Actual endpoint URL will be provided via the bot itself as a part of `/token` command output. @@ -30,12 +30,12 @@ Request's body has this structure: These two parameters are used for a text notification: * `message` is the text of the message to be sent via the bot. Maximum length is 4096 (also happens to be a maximum length of one Telegram message). -* `type` is used to select between two supported text parse modes: `"plaintext"` for plain text, and `"markdown"` for MarkdownV2 as described in [Telegram docs](https://core.telegram.org/bots/api#markdownv2-style). Optional parameter, if value is not supplied, defaults to `"plaintext"`. These two are separated, because Telegram flavoured Markdown requires escaping for a fairly common plaintext punctuation marks, and will fail if not formed correctly. +* `type` is used to select between two supported text parse modes: `"plaintext"` for plain text, and `"markdown"` for MarkdownV2 as described in [Telegram docs](https://core.telegram.org/bots/api#markdownv2-style). Optional parameter, if value is not supplied, defaults to `"plaintext"`. These two are separated, because Telegram flavoured Markdown requires escaping for a fairly common plaintext punctuation marks, and will fail if not formed correctly. -This parameter should be provided for a sticker notification: -* `sticker` is an internal Telegram ID of a sticker. For obtaining values to use, see [Sticker identification](#sticker-identification). +This parameter should be provided for a sticker notification: +* `sticker` is an internal Telegram ID of a sticker. For obtaining values to use, see [Sticker identification](#sticker-identification). -Providing both `message` and `sticker` at the same time is considered a bad request. When `sticker` is present, `type` is ignored. +Providing both `message` and `sticker` at the same time is considered a bad request. When `sticker` is present, `type` is ignored. #### Response API returns an empty HTTP response with any of the following status codes: @@ -51,7 +51,7 @@ Client should retry later, the included `Retry-After` header suggests the amount Client can try to retry later, but ¯\\\_(ツ)\_/¯ #### Rate limitation -The API has a rate limitation, preventing large amount of notifications in a short amount of time. By default _([configurable](#bandwidth-section)),_ every user can send up to _20_ notifications. Every notification sent increases the notifications sent counter by one, and requests will be refused with `429 Too Many Requests` status code when counter is maxed. A single point is subtracted from the counter every _minute_ after last message was sent. +The API has a rate limitation, preventing large amount of notifications in a short amount of time. By default _([configurable](#bandwidth-section)),_ every user can send up to _20_ notifications. Every notification sent increases the notifications sent counter by one, and requests will be refused with `429 Too Many Requests` status code when counter is maxed. A single point is subtracted from the counter every _minute_ after last message was sent. For instance, if API is used to send 40 notifications in quick succession, only the 20 first messages will be sent to the user. If the client waits 5 minutes after API starts responding with 429's, they will be able to send 5 more messages before hitting the limit again. After 20 minutes of idle time since the last successfully sent message, the API will behave as usual. @@ -66,10 +66,12 @@ Available commands: * `/regenerate` -- once confirmed (see `/confirm` and `/cancel`) replaces user's token with a new one; * `/delete` -- once confirmed removes user's token; * `/confirm` -- confirms regeneration or deletion; -* `/cancel` -- cancels regeneration or deletion. +* `/cancel` -- cancels regeneration or deletion. When there is a destructive (either regeneration or deletion) operation pending, only `/cancel` or `/confirm` commands are accepted. +There is an option to automatically add these commands to the bot's command list visible in the dialog with the bot; see `FillCommands` in [general configuration](#general-section). + #### Registration limitation The ability for anyone to create a token for themselves is toggleable via config entry. You can always run direct queries against bot's DB for quick editing. Note that messages will not be delivered unless user actually engaged in a conversation with the bot if an entry was created via DB edits. @@ -77,8 +79,18 @@ The ability for anyone to create a token for themselves is toggleable via config Bot will reply with a message containing the ID of any sticker sent to it. This value could be used to send notifications containing a sticker instead of text. This works even when there is a pending confirmation which usually blocks most of the commands. +### Web page(s) +The app also includes a static index page with a minigame. This page can also be used as a health check: if it is available, the whole app is probably up and running. + +There's also a glorious error page shared with some of my other web projects. + +#### Minigame +The included minigame is a simple implementation of a classic 15 Puzzle. Click or tap a tile adjacent to the empty space to move it there. "Shuffle" button randomly shuffles the board (only using valid moves, so it is always can be solved); "Reset" button resets the field. + +There's no congratulatory message when the puzzle is solved, as the game state is not tracked in any way. + ## Configuration and deployment -You'll need .NET 8 runtime. Hosting on GNU/Linux in a proper data center is highly encouraged. +You'll need .NET 8 runtime. Hosting on GNU/Linux in a proper data center is highly encouraged. ### Configuration The settings are stored inside `appsettings.json`, app-specific options are defined in "General" and "Bandwidth" sections. @@ -109,3 +121,5 @@ Shelved attempt to improve the codebase. Consists of one architecture change and Really proper markdown support this time (Telegram's version with questionable selection of characters to be escaped), option to send a silent notification, async everthing, .NET 7, HTTP status codes instead of custom errors, and probably something else I forgot about. * **v 2.1**, 2023-05-12 Support for sending stickers instead of text messages. +* **v 2.2**, (hopefully) 2024-05-xx +Upcoming version with some improvements, inspired by the rewrite of a similar project. Includes (LTS) .NET 8, better architecture, some web pages; stores all its data in the database. \ No newline at end of file From f1abfdcd416d515d1e0628aab170097285435875 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Tue, 14 May 2024 21:29:58 +0700 Subject: [PATCH 32/37] index minigame: reimplemented from earlier prototypes, this time with a mobile layout that's even somewhat playable on my phone! --- WebToTelegramCore/Views/Home/Index.cshtml | 26 +++- WebToTelegramCore/wwwroot/css/home.css | 153 +++++++++++++++++++++- WebToTelegramCore/wwwroot/css/shared.css | 12 ++ WebToTelegramCore/wwwroot/js/game.js | 106 ++++++++++++++- 4 files changed, 288 insertions(+), 9 deletions(-) diff --git a/WebToTelegramCore/Views/Home/Index.cshtml b/WebToTelegramCore/Views/Home/Index.cshtml index 9b693d0..1205953 100644 --- a/WebToTelegramCore/Views/Home/Index.cshtml +++ b/WebToTelegramCore/Views/Home/Index.cshtml @@ -1,10 +1,22 @@ - - - soon™ - - - soon™ - + + + Dotnet Telegram forwarder home page + + + + + +
+

Dotnet Telegram forwarder online

+

Since you're, here's a game! Click/tap a tile adjacent to the empty space to move it. Use buttons below to (de)scramble the field.

+
+
+
+ + +
+
+ diff --git a/WebToTelegramCore/wwwroot/css/home.css b/WebToTelegramCore/wwwroot/css/home.css index 99b934e..aeb8214 100644 --- a/WebToTelegramCore/wwwroot/css/home.css +++ b/WebToTelegramCore/wwwroot/css/home.css @@ -1 +1,152 @@ -/* soon™ */ +.container { + /* imagine doing pixel-centered designs in this day and age */ + width: 600px; +} + +h1 { + font-family: var(--font-mono); + margin: 2em 0; + text-align: center; +} + +.subtitle { + text-align: start; +} + +.separator { + border-top: 1px solid var(--fg); +} + +.subtitle, .separator { + width: 100%; + margin-bottom: 12px; +} + +.field { + width: 400px; + height: 400px; + background-color: var(--bg-accent); + position: relative; + border-left: 4px solid var(--bg-accent); + border-top: 4px solid var(--bg-accent); +} + +.field > .cell { + position: absolute; + width: 25%; + height: 25%; + display: flex; + + transition-property: top, left; + transition-duration: 200ms, 200ms; + transition-timing-function: ease-in-out; +} + +.field > .cell > .wrapper { + margin: 0 4px 4px 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: initial; + width: 100%; + background-color: var(--bg); + border: 1px solid var(--fg); +} + +.field > .cell > .wrapper > .text { + font-family: var(--font-mono); + font-size: 36pt; + color: var(--fg); + user-select: none; + text-transform: capitalize; +} + +.field > .cell.row0 { + top: 0; +} + +.field > .cell.row1 { + top: 25%; +} + +.field > .cell.row2 { + top: 50%; +} + +.field > .cell.row3 { + top: 75%; +} + +.field > .cell.col0 { + left: 0; +} + +.field > .cell.col1 { + left: 25%; +} + +.field > .cell.col2 { + left: 50%; +} + +.field > .cell.col3 { + left: 75%; +} + +.buttons { + margin-top: 2em; + width: 400px; + display: flex; + justify-content: space-around; +} + +.buttons > button { + width: 33%; + padding: 4px 0; +} + +/* mobile layout; better than nothing at least */ +@media only screen and (max-device-width: 480px) { + + body { + font-size: 24pt; + } + + .container { + width: 100%; + } + + h1 { + margin: 1em 0; + } + + .separator { + border-width: 4px; + } + + .field { + width: 100vw; + height: 100vw; + border-left: 0; + border-top: 0; + } + + .field > .cell > .wrapper { + margin: 0 8px 8px 0; + border-width: 4px; + } + + .field > .cell > .wrapper > .text { + font-size: 72pt; + } + + .buttons { + width: 100vw; + height: 2em; + } + + .buttons button { + font-size: 24pt; + } +} diff --git a/WebToTelegramCore/wwwroot/css/shared.css b/WebToTelegramCore/wwwroot/css/shared.css index f8a81ce..458a9ec 100644 --- a/WebToTelegramCore/wwwroot/css/shared.css +++ b/WebToTelegramCore/wwwroot/css/shared.css @@ -43,4 +43,16 @@ body { --bg-accent: #222; --fg: #ccc; } + + a:link { + color: #a0a0ff; + } + + a:visited { + color: #f0a0f0; + } + + a:active { + color: #fff; + } } diff --git a/WebToTelegramCore/wwwroot/js/game.js b/WebToTelegramCore/wwwroot/js/game.js index efbdd0c..e6dc15f 100644 --- a/WebToTelegramCore/wwwroot/js/game.js +++ b/WebToTelegramCore/wwwroot/js/game.js @@ -1 +1,105 @@ -// to be implemented later +const field_width = 4; +const field_height = 4; +// null indicates the empty space +let gameState = [...Array(15).keys(), null]; + +// left, right, top, bottom +// delta is the change in index for the move +// cond is move validity condition (can't move left from leftmost column etc) +const directions = Object.freeze([ + { delta: -1, cond: (index) => (index % field_width) !== 0 }, + { delta: 1, cond: (index) => (index % field_width) !== (field_width - 1) }, + { delta: -field_width, cond: (index) => index >= field_width }, + { delta: field_width, cond: (index) => index < (field_width * (field_height - 1)) } +]); + +onLoad = () => { + const field = document.getElementById("field"); + + for (let i = 0; i < field_height; i++) { + for (let j = 0; j < field_width; j++) { + const index = i * field_height + j; + + if (gameState[index] === null) { + continue; + } + const cell = document.createElement("div"); + cell.value = gameState[index]; + cell.displayValue = gameState[index] + 1; + cell.id = cell.value.toString(); + cell.onclick = () => onCellClick(cell); + cell.className = `cell row${i} col${j}`; + cell.innerHTML = `
${cell.displayValue.toString(16)}
`; + field.appendChild(cell); + } + } +} + +onCellClick = (cell) => { + for (let i = 0; i < directions.length; i++) + { + const dir = directions[i]; + if (canMove(cell.value, dir)) { + move(cell.value, dir); + break; + } + } +} + +canMove = (value, direction) => { + const index = gameState.indexOf(value); + if (value !== null) { + return direction.cond(index) && gameState[index + direction.delta] === null; + } else { + return direction.cond(index) && gameState[index + direction.delta] !== null; + } +} + +move = (value, direction) => { + const originalIndex = gameState.indexOf(value); + const newIndex = originalIndex + direction.delta; + + const tmp = gameState[newIndex]; + gameState[newIndex] = gameState[originalIndex]; + gameState[originalIndex] = tmp; + // the null is only moved when the field is scrambled; + // the classes are only reassigned at the end of the entire scramble cycle + if (value !== null) { + const cell = document.getElementById(value.toString()); + cell.className = `cell row${Math.floor(newIndex / field_width)} col${newIndex % field_width}`; + } +} + +reset = () => { + gameState = [...Array(15).keys(), null]; + assignClasses(); +} + +shuffle = () => { + // just moves the empty space randomly enough times + for (let i = 0; i < 8192; i++) { + let validDirs = []; + for (let j = 0; j < directions.length; j++) { + if (canMove(null, directions[j])) { + validDirs.push(directions[j]); + } + } + const randomIndex = Math.floor(Math.random() * validDirs.length); + move(null, validDirs[randomIndex]); + } + assignClasses(); +} + +assignClasses = () => { + for (let i = 0; i < field_height; i++) { + for (let j = 0; j < field_width; j++) { + const index = i * field_height + j; + + if (gameState[index] === null) { + continue; + } + const cell = document.getElementById(gameState[index].toString()); + cell.className = `cell row${i} col${j}`; + } + } +} From 08fcfc956d112a2c91868b33abf12dc6dd37e9df Mon Sep 17 00:00:00 2001 From: Bn4 Date: Tue, 14 May 2024 21:45:39 +0700 Subject: [PATCH 33/37] readme: minor changes --- readme.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 5b49999..4e09f8b 100644 --- a/readme.md +++ b/readme.md @@ -4,10 +4,10 @@ _(also known as WebToTelegramCore internally)_ An app providing HTTP API to deliver arbitrary text or sticker notifications via associated Telegram bot from anywhere where making HTTP POST requests is available. ## Status -Operational. In continous service since mid 2018. +Operational. In continuous service since mid 2018. ## Description -Let's try [readme driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) this time. This app consists of two equally important parts: [Telegram bot](#telegram-bot) and a [web API](#web-api); and a less important [web page](#web-pages) part. +Let's try [readme driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) this time. This app consists of two equally important parts: [Telegram bot](#telegram-bot) and a [web API](#web-api); and a less important [web](#web-pages) part. ### Web API Has only one method to send the notification. Its endpoint listens for POST requests with JSON body. Actual endpoint URL will be provided via the bot itself as a part of `/token` command output. @@ -70,7 +70,7 @@ Available commands: When there is a destructive (either regeneration or deletion) operation pending, only `/cancel` or `/confirm` commands are accepted. -There is an option to automatically add these commands to the bot's command list visible in the dialog with the bot; see `FillCommands` in [general configuration](#general-section). +There is a config option to automatically add these commands to the bot's command list visible in the dialog with the bot; see `FillCommands` in [general configuration](#general-section). #### Registration limitation The ability for anyone to create a token for themselves is toggleable via config entry. You can always run direct queries against bot's DB for quick editing. Note that messages will not be delivered unless user actually engaged in a conversation with the bot if an entry was created via DB edits. @@ -85,15 +85,15 @@ The app also includes a static index page with a minigame. This page can also be There's also a glorious error page shared with some of my other web projects. #### Minigame -The included minigame is a simple implementation of a classic 15 Puzzle. Click or tap a tile adjacent to the empty space to move it there. "Shuffle" button randomly shuffles the board (only using valid moves, so it is always can be solved); "Reset" button resets the field. +The included minigame is a simple implementation of a classic 15 Puzzle. Click or tap a tile adjacent to the empty space to move it there. "Shuffle" button randomly shuffles the board (only using valid moves, so the resulting field is always solvable); "Reset" button resets the field to initial state. There's no congratulatory message when the puzzle is solved, as the game state is not tracked in any way. ## Configuration and deployment -You'll need .NET 8 runtime. Hosting on GNU/Linux in a proper data center is highly encouraged. +You'll need .NET 8 runtime. Hosting on a GNU/Linux machine in a proper data center is highly encouraged. ### Configuration -The settings are stored inside `appsettings.json`, app-specific options are defined in "General" and "Bandwidth" sections. +The settings are stored inside `appsettings.json`, app-specific options are defined in "General" and "Bandwidth" sections. The rest of the file is usual ASP.NET settings. #### General section "General" section contains: @@ -122,4 +122,4 @@ Really proper markdown support this time (Telegram's version with questionable s * **v 2.1**, 2023-05-12 Support for sending stickers instead of text messages. * **v 2.2**, (hopefully) 2024-05-xx -Upcoming version with some improvements, inspired by the rewrite of a similar project. Includes (LTS) .NET 8, better architecture, some web pages; stores all its data in the database. \ No newline at end of file +Upcoming version with some improvements, inspired by the rewrite of a similar project. Includes (LTS) .NET 8 runtime, better architecture, status page with a minigame; unlike older versions, this one stores all its data in the database. \ No newline at end of file From b84435b3e3d0283bdd97123c0487b9179b9b196e Mon Sep 17 00:00:00 2001 From: Bn4 Date: Fri, 31 May 2024 20:42:33 +0700 Subject: [PATCH 34/37] fixed changes to Record.State not being saved in some cases guess this is what i get for "nah, i'll test the whole thing when it's done" --- WebToTelegramCore/BotCommands/CancelCommand.cs | 11 ++++++++++- WebToTelegramCore/BotCommands/DeleteCommand.cs | 9 ++++++++- WebToTelegramCore/BotCommands/RegenerateCommand.cs | 11 ++++++++++- .../Services/BotCommandFactoryService.cs | 6 +++--- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/WebToTelegramCore/BotCommands/CancelCommand.cs b/WebToTelegramCore/BotCommands/CancelCommand.cs index 0569541..15d27c4 100644 --- a/WebToTelegramCore/BotCommands/CancelCommand.cs +++ b/WebToTelegramCore/BotCommands/CancelCommand.cs @@ -18,10 +18,18 @@ public class CancelCommand : ConfirmationCommandBase, IBotCommand public override string Description => Locale.CancelDescription; + /// + /// Database context to use. + /// + private readonly RecordContext _context; + /// /// Constructor. /// - public CancelCommand() : base() { } + public CancelCommand(RecordContext context) : base() + { + _context = context; + } /// /// Method to process the command. Resets Record's State back to Normal. @@ -42,6 +50,7 @@ public override string Process(Record record) : Locale.CancelRegeneration; record.State = RecordState.Normal; + _context.SaveChanges(); return reply; } } diff --git a/WebToTelegramCore/BotCommands/DeleteCommand.cs b/WebToTelegramCore/BotCommands/DeleteCommand.cs index 9d4195e..71fbafa 100644 --- a/WebToTelegramCore/BotCommands/DeleteCommand.cs +++ b/WebToTelegramCore/BotCommands/DeleteCommand.cs @@ -24,13 +24,19 @@ public class DeleteCommand : UserOnlyCommandBase, IBotCommand /// private readonly bool _registrationEnabled; + /// + /// Database context to use. + /// + private readonly RecordContext _context; + /// /// Constructor. /// /// Registration state. True is enabled. - public DeleteCommand(bool registrationEnabled) : base() + public DeleteCommand(RecordContext context, bool registrationEnabled) : base() { _registrationEnabled = registrationEnabled; + _context = context; } /// @@ -51,6 +57,7 @@ public override string Process(Record record) private string InternalProcess(Record record) { record.State = RecordState.PendingDeletion; + _context.SaveChanges(); return _registrationEnabled ? Locale.DeletionPending : Locale.DeletionPending + "\n\n" + Locale.DeletionNoTurningBack; diff --git a/WebToTelegramCore/BotCommands/RegenerateCommand.cs b/WebToTelegramCore/BotCommands/RegenerateCommand.cs index 0a661c9..665b0dd 100644 --- a/WebToTelegramCore/BotCommands/RegenerateCommand.cs +++ b/WebToTelegramCore/BotCommands/RegenerateCommand.cs @@ -18,10 +18,18 @@ public class RegenerateCommand : UserOnlyCommandBase, IBotCommand public override string Description => Locale.RegenerateDescription; + /// + /// Database context to use. + /// + private readonly RecordContext _context; + /// /// Constructor. /// - public RegenerateCommand() : base() { } + public RegenerateCommand(RecordContext context) : base() + { + _context = context; + } /// /// Method to process the command. @@ -41,6 +49,7 @@ public override string Process(Record record) private string InternalProcess(Record record) { record.State = RecordState.PendingRegeneration; + _context.SaveChanges(); return Locale.RegenerationPending; } } diff --git a/WebToTelegramCore/Services/BotCommandFactoryService.cs b/WebToTelegramCore/Services/BotCommandFactoryService.cs index aa0345c..3708812 100644 --- a/WebToTelegramCore/Services/BotCommandFactoryService.cs +++ b/WebToTelegramCore/Services/BotCommandFactoryService.cs @@ -24,10 +24,10 @@ public class BotCommandFactoryService(IOptions options, RecordCon [ new TokenCommand(options.Value.ApiEndpointUrl), new CreateCommand(context, generator, options.Value.RegistrationEnabled), - new DeleteCommand(options.Value.RegistrationEnabled), - new RegenerateCommand(), + new DeleteCommand(context, options.Value.RegistrationEnabled), + new RegenerateCommand(context), new ConfirmCommand(context, generator), - new CancelCommand(), + new CancelCommand(context), new HelpCommand(), new AboutCommand(), From 832987e262a416714a1a99f3d40179de6b258a51 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Fri, 31 May 2024 21:09:45 +0700 Subject: [PATCH 35/37] actually bumped version to 2.2; removed unneeded escape of n-dash as opposed to minus in about command --- WebToTelegramCore/Resources/Locale.cs | 2 +- WebToTelegramCore/WebToTelegramCore.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WebToTelegramCore/Resources/Locale.cs b/WebToTelegramCore/Resources/Locale.cs index 33700f9..10c4a83 100644 --- a/WebToTelegramCore/Resources/Locale.cs +++ b/WebToTelegramCore/Resources/Locale.cs @@ -20,7 +20,7 @@ public static class Locale **Dotnet Telegram forwarder** v {0}\. [Open\-source\!](https://github.com/bnfour/dotnet-telegram-forwarder) - by bnfour, 2018, 2020\–2024\. + by bnfour, 2018, 2020–2024\. """; /// diff --git a/WebToTelegramCore/WebToTelegramCore.csproj b/WebToTelegramCore/WebToTelegramCore.csproj index ca53426..d7fb7ec 100644 --- a/WebToTelegramCore/WebToTelegramCore.csproj +++ b/WebToTelegramCore/WebToTelegramCore.csproj @@ -2,7 +2,7 @@ net8.0 - 2.1.0.0 + 2.2.0.0 bnfour Dotnet Telegram forwarder © 2018, 2020–2024, bnfour From 3ff92db9331e148a8bad4482989363c9216070b0 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Fri, 31 May 2024 21:55:42 +0700 Subject: [PATCH 36/37] changed all paths to relative because i totally forgot about the way this app is deployed on my box and everything but the page itself refused to load --- WebToTelegramCore/Views/ErrorPage/OnGet.cshtml | 8 ++++---- WebToTelegramCore/Views/Home/Index.cshtml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml b/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml index f1cf4b4..540343b 100644 --- a/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml +++ b/WebToTelegramCore/Views/ErrorPage/OnGet.cshtml @@ -3,18 +3,18 @@ @Model.StatusCode @Model.Description - - + + - +
@await Html.PartialAsync("_ErrorSvg")

@Model.StatusCode @Model.Description

- home page + home page
diff --git a/WebToTelegramCore/Views/Home/Index.cshtml b/WebToTelegramCore/Views/Home/Index.cshtml index 1205953..b26c0e0 100644 --- a/WebToTelegramCore/Views/Home/Index.cshtml +++ b/WebToTelegramCore/Views/Home/Index.cshtml @@ -3,9 +3,9 @@ Dotnet Telegram forwarder home page - - - + + +
From 08ad9d6ef35adb337ca78c18aae114a4e44b9225 Mon Sep 17 00:00:00 2001 From: Bn4 Date: Fri, 31 May 2024 22:05:04 +0700 Subject: [PATCH 37/37] readme: changelog entry for 2.2 --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 4e09f8b..3d834b9 100644 --- a/readme.md +++ b/readme.md @@ -7,7 +7,7 @@ An app providing HTTP API to deliver arbitrary text or sticker notifications via Operational. In continuous service since mid 2018. ## Description -Let's try [readme driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) this time. This app consists of two equally important parts: [Telegram bot](#telegram-bot) and a [web API](#web-api); and a less important [web](#web-pages) part. +Let's try [readme driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) this time. This app consists of two equally important parts: [Telegram bot](#telegram-bot) and a [web API](#web-api); and an (arguably) less important [web](#web-pages) part. ### Web API Has only one method to send the notification. Its endpoint listens for POST requests with JSON body. Actual endpoint URL will be provided via the bot itself as a part of `/token` command output. @@ -121,5 +121,5 @@ Shelved attempt to improve the codebase. Consists of one architecture change and Really proper markdown support this time (Telegram's version with questionable selection of characters to be escaped), option to send a silent notification, async everthing, .NET 7, HTTP status codes instead of custom errors, and probably something else I forgot about. * **v 2.1**, 2023-05-12 Support for sending stickers instead of text messages. -* **v 2.2**, (hopefully) 2024-05-xx -Upcoming version with some improvements, inspired by the rewrite of a similar project. Includes (LTS) .NET 8 runtime, better architecture, status page with a minigame; unlike older versions, this one stores all its data in the database. \ No newline at end of file +* **v 2.2**, 2024-05-31 +Some improvements inspired by an earlier rewrite of a related project, [tg-bots-dotnet](https://github.com/bnfour/tg-bots-dotnet). Includes (LTS) .NET 8 runtime, (probably) better architecture, status page with a minigame; unlike older versions, all the data is now stored in the database.