From a8a7047af59a919ba5d1f104ba1db0f6807c9345 Mon Sep 17 00:00:00 2001 From: Ronny Gunawan <3048897+ronnygunawan@users.noreply.github.com> Date: Sat, 7 Dec 2024 12:25:48 +0700 Subject: [PATCH] Cleanup linter resharper warnings --- .editorconfig | 21 +- .../inspectionProfiles/Project_Default.xml | 6 + .../AI/Gemini/GeminiTextPromptHandler.cs | 184 +++--- .../AI/OpenAI/AskCommandHandler.cs | 34 +- .../OpenAIImageGenerationPromptHandler.cs | 30 +- .../AI/OpenAI/OpenAIImagePromptHandler.cs | 67 +- .../AI/OpenAI/OpenAITextPromptHandler.cs | 61 +- .../AI/RateLimit/AIRateLimiters.cs | 4 +- .../StabilityTextToImagePromptHandler.cs | 21 +- .../Art/ArtCommandHandler.cs | 23 +- .../BMKG/BMKGCommandHandler.cs | 19 +- .../CallbackQueryUpdateHandler.cs | 4 +- .../InlineQuery/InlineQueryUpdateHandler.cs | 12 +- .../BotUpdate/Message/AICallCommandHandler.cs | 38 +- .../Message/AIFollowUpMessageHandler.cs | 33 +- .../BotUpdate/Message/MessageUpdateHandler.cs | 293 ++++----- .../BotUpdate/Message/SlashCommandHandler.cs | 38 +- .../Eval/EvalCommandHandler.cs | 24 +- .../Exec/ExecCommandHandler.cs | 22 +- .../FlipFlop/FlipFlopCommandHandler.cs | 6 +- .../Fuck/FuckCommandHandler.cs | 10 +- .../GoogleMaps/MapCommandHandler.cs | 19 +- .../Humor/HumorCommandHandler.cs | 13 +- .../Khodam/KhodamCommandHandler.cs | 4 +- .../Pop/BubbleWrapCallbackHandler.cs | 7 +- .../Pop/PopCommandHandler.cs | 6 +- .../Primbon/PrimbonCommandHandler.cs | 37 +- .../Privilege/PrivilegeCommandHandler.cs | 29 +- .../SQL/SQLCommandHandler.cs | 62 +- .../TelegramMessageCache.cs | 19 +- .../Weather/WeatherCommandHandler.cs | 18 +- BotNet.Commands/AI/Gemini/GeminiTextPrompt.cs | 58 +- .../AI/OpenAI/OpenAIImageGenerationPrompt.cs | 4 +- .../AI/OpenAI/OpenAIImagePrompt.cs | 39 +- BotNet.Commands/AI/OpenAI/OpenAITextPrompt.cs | 60 +- BotNet.Commands/BMKG/BMKGCommand.cs | 6 +- .../BotUpdate/Message/AICallCommand.cs | 8 +- .../BotUpdate/Message/AIFollowUpMessage.cs | 32 +- .../BotUpdate/Message/AIResponseMessage.cs | 6 +- BotNet.Commands/ChatAggregate/Chat.cs | 20 +- .../CommandPrioritizationOptions.cs | 4 +- .../CommandPriorityCategorizer.cs | 4 +- BotNet.Commands/SQL/SQLCommand.cs | 12 +- BotNet.Commands/SenderAggregate/Sender.cs | 25 +- BotNet.Services/BMKG/BMKG.cs | 10 +- BotNet.Services/BMKG/EarthQuake.cs | 9 +- BotNet.Services/BMKG/LatestEarthQuake.cs | 34 +- .../BMKG/ServiceCollectionExtensions.cs | 2 +- BotNet.Services/BotCommands/OpenAI.cs | 576 ------------------ BotNet.Services/BotCommands/Preview.cs | 88 --- .../BotProfile/BotProfileAccessor.cs | 4 +- .../Brainfuck/BrainfuckInterpreter.cs | 5 +- .../Brainfuck/BrainfuckTranspiler.cs | 28 +- .../BubbleWrap/BubbleWrapKeyboardGenerator.cs | 21 +- BotNet.Services/BubbleWrap/BubbleWrapSheet.cs | 4 +- .../ChineseCalendar/ChineseCalendarScraper.cs | 7 +- .../JsonConverters/ScriptObjectConverter.cs | 2 +- .../JsonConverters/UndefinedConverter.cs | 2 +- BotNet.Services/ClearScript/V8Evaluator.cs | 25 +- BotNet.Services/ClearScript/V8Options.cs | 4 +- .../ColorCard/ColorCardRenderer.cs | 27 +- BotNet.Services/CopyPasta/CopyPastaLookup.cs | 6 +- BotNet.Services/Craiyon/CraiyonClient.cs | 46 +- .../DynamicExpresso/CSharpEvaluator.cs | 14 +- .../ServiceCollectionExtensions.cs | 24 +- .../FancyText/FancyTextGenerator.cs | 21 +- BotNet.Services/Gemini/GeminiClient.cs | 23 +- BotNet.Services/Giphy/GiphyClient.cs | 6 +- BotNet.Services/GoogleMap/GeoCode.cs | 23 +- BotNet.Services/GoogleMap/Models/Geometry.cs | 1 + .../GoogleMap/Models/LocationType.cs | 3 +- BotNet.Services/GoogleMap/Models/Result.cs | 1 + .../GoogleMap/Models/StatusCode.cs | 3 +- BotNet.Services/GoogleMap/StaticMap.cs | 12 +- .../GoogleSheets/FromColumnAttribute.cs | 12 +- .../GoogleSheets/GoogleSheetsClient.cs | 16 +- .../ImageConverter/IcoToPngConverter.cs | 25 +- BotNet.Services/Json/SnakeCaseNamingPolicy.cs | 18 +- BotNet.Services/Khodam/KhodamCalculator.cs | 10 +- .../ServiceCollectionExtensions.cs | 4 +- .../KokizzuVPSBenchmark/VPSBenchmark.cs | 2 +- .../VPSBenchmarkDataSource.cs | 13 +- .../MarkdownV2/MarkdownV2Sanitizer.cs | 16 +- BotNet.Services/Meme/MemeGenerator.cs | 31 +- BotNet.Services/Meme/Templates.cs | 2 +- BotNet.Services/OpenAI/AttachmentGenerator.cs | 4 +- BotNet.Services/OpenAI/IntentDetector.cs | 8 +- BotNet.Services/OpenAI/OpenAIClient.cs | 153 +++-- BotNet.Services/OpenAI/OpenAIOptions.cs | 4 +- .../OpenAI/OpenAIStreamingClient.cs | 22 +- .../OpenAI/ServiceCollectionExtensions.cs | 6 +- BotNet.Services/OpenAI/Skills/AssistantBot.cs | 43 +- BotNet.Services/OpenAI/Skills/FriendlyBot.cs | 75 ++- .../OpenAI/Skills/ImageGenerationBot.cs | 11 +- BotNet.Services/OpenAI/Skills/SarcasticBot.cs | 10 +- .../OpenAI/Skills/TldrGenerator.cs | 6 +- BotNet.Services/OpenAI/Skills/Translator.cs | 8 +- BotNet.Services/OpenAI/Skills/VisionBot.cs | 8 +- BotNet.Services/OpenAI/ThreadTracker.cs | 6 +- .../Pemilu2024/PilegDPRDapilDataSource.cs | 118 ++-- .../Pemilu2024/PilegDPRPerDapilDataSource.cs | 126 ++-- .../PilegDPRPerProvinsiDataSource.cs | 122 ++-- .../Pemilu2024/PilpresDataSource.cs | 22 +- .../Pemilu2024/ServiceCollectionExtensions.cs | 180 +++--- BotNet.Services/Pemilu2024/SirekapClient.cs | 36 +- BotNet.Services/Pemilu2024/Types.cs | 18 +- .../Pesto/Exceptions/PestoAPIException.cs | 7 +- .../Exceptions/PestoEmptyCodeException.cs | 4 +- .../PestoMonthlyLimitExceededException.cs | 4 +- .../PestoRuntimeNotFoundException.cs | 7 +- .../PestoServerRateLimitedException.cs | 4 +- BotNet.Services/Pesto/Models/Language.cs | 8 +- BotNet.Services/Pesto/PestoClient.cs | 73 +-- BotNet.Services/Piston/PistonClient.cs | 2 +- BotNet.Services/Preview/YoutubePreview.cs | 60 +- BotNet.Services/Primbon/PrimbonScraper.cs | 21 +- .../ProgrammerHumor/ProgrammerHumorScraper.cs | 6 +- .../RateLimit/Internal/PerChatRateLimiter.cs | 21 +- .../Internal/PerUserPerChatRateLimiter.cs | 21 +- .../RateLimit/Internal/PerUserRateLimiter.cs | 21 +- .../RateLimit/RateLimitExceededException.cs | 10 +- BotNet.Services/RateLimit/RateLimiter.cs | 2 - BotNet.Services/Sqlite/ScopedDatabase.cs | 1 - .../Models/ContentFilteredException.cs | 8 +- .../Stability/Skills/ImageGenerationBot.cs | 4 +- .../Stability/Skills/ImageVariationBot.cs | 4 +- BotNet.Services/Stability/StabilityClient.cs | 41 +- .../TelegramBotClientResilienceExtensions.cs | 8 +- .../ThisArtworkDoesNotExist.cs | 14 +- .../ThisXDoesNotExist/ThisCatDoesNotExist.cs | 14 +- .../ThisXDoesNotExist/ThisIdeaDoesNotExist.cs | 4 +- BotNet.Services/Tiktok/TiktokLinkSanitizer.cs | 5 +- .../Tokopedia/TokopediaLinkSanitizer.cs | 7 +- .../Typography/BotNetFontService.cs | 12 +- BotNet.Services/Typography/FontStyle.cs | 2 +- BotNet.Services/Weather/CurrentWeather.cs | 46 +- BotNet.Services/Weather/Models/Current.cs | 3 +- BotNet.Services/Weather/Weather.cs | 21 +- BotNet.Tests/BotNet.Tests.csproj | 1 - .../SocialLink/SocialLinkEmbedFixerTests.cs | 5 +- BotNet.Tests/TestUtilities/HttpClientMock.cs | 7 +- BotNet.sln.DotSettings | 254 ++++++++ BotNet.sln.DotSettings.user | 7 + BotNet/Bot/BotService.cs | 32 +- BotNet/Bot/CommandConsumer.cs | 16 +- BotNet/Bot/UpdateHandler.cs | 15 +- BotNet/Program.cs | 8 +- .../DecimalClock/DecimalClockSvgBuilder.cs | 8 +- BotNet/Views/DecimalClock/Svg.cshtml | 10 +- README.md | 16 +- pehape | 2 +- 151 files changed, 1920 insertions(+), 2534 deletions(-) create mode 100644 .idea/.idea.BotNet/.idea/inspectionProfiles/Project_Default.xml delete mode 100644 BotNet.Services/BotCommands/OpenAI.cs delete mode 100644 BotNet.Services/BotCommands/Preview.cs create mode 100644 BotNet.sln.DotSettings create mode 100644 BotNet.sln.DotSettings.user diff --git a/.editorconfig b/.editorconfig index 467392d..3ecfbf2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -85,30 +85,24 @@ dotnet_naming_symbols.method_symbols.required_modifiers = async dotnet_naming_rule.public_members_must_be_capitalized.severity = error dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols dotnet_naming_rule.public_members_must_be_capitalized.style = pascal_case_style -dotnet_naming_symbols.public_symbols.applicable_kinds = property,method,field,event,delegate +dotnet_naming_symbols.public_symbols.applicable_kinds = property,method,field,event,delegate,tuples dotnet_naming_symbols.public_symbols.applicable_accessibilities = public,internal,protected,protected_internal -# Named tuples must be pascal case -dotnet_naming_rule.public_members_must_be_capitalized.severity = warning -dotnet_naming_rule.public_members_must_be_capitalized.symbols = named_tuples -dotnet_naming_rule.public_members_must_be_capitalized.style = pascal_case_style -dotnet_naming_symbols.named_tuples.applicable_kinds = tuples -dotnet_naming_symbols.named_tuples.applicable_accessibilities = public,internal,protected,protected_internal # Fields must be camel case prefixed with an underscore dotnet_naming_rule.non_public_members_must_be_underscored_camel_case.severity = warning dotnet_naming_rule.non_public_members_must_be_underscored_camel_case.symbols = fields dotnet_naming_rule.non_public_members_must_be_underscored_camel_case.style = underscore_camel_case_style dotnet_naming_symbols.fields.applicable_kinds = field dotnet_naming_symbols.fields.applicable_accessibilities = private -# Constants must be macro case +# Constants must be pascal case dotnet_naming_rule.constant_fields_should_be_upper_case.severity = error dotnet_naming_rule.constant_fields_should_be_upper_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_upper_case.style = macro_case_style +dotnet_naming_rule.constant_fields_should_be_upper_case.style = pascal_case_style dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.required_modifiers = const -# Static readonly fields must be macro case +# Static readonly fields must be pascal case dotnet_naming_rule.static_readonly_fields_should_be_upper_case.severity = error dotnet_naming_rule.static_readonly_fields_should_be_upper_case.symbols = static_readonly_fields -dotnet_naming_rule.static_readonly_fields_should_be_upper_case.style = macro_case_style +dotnet_naming_rule.static_readonly_fields_should_be_upper_case.style = pascal_case_style dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field dotnet_naming_symbols.static_readonly_fields.required_modifiers = static,readonly # Locals must be camel case @@ -153,7 +147,7 @@ csharp_style_inlined_variable_declaration = true:suggestion # C# Formatting Rules # ############################### # New line preferences -csharp_new_line_before_open_brace = false +csharp_new_line_before_open_brace = none csharp_new_line_before_else = false csharp_new_line_before_catch = false csharp_new_line_before_finally = false @@ -214,9 +208,6 @@ dotnet_diagnostic.RCS1118.severity = warning # RCS1169: Make field read-only. dotnet_diagnostic.RCS1169.severity = warning -# IDE0011: Add braces -csharp_prefer_braces = when_multiline:warning - # RCS1001: Add braces (when expression spans over multiple lines). dotnet_diagnostic.RCS1001.severity = warning diff --git a/.idea/.idea.BotNet/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.BotNet/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..5cb71ef --- /dev/null +++ b/.idea/.idea.BotNet/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/BotNet.CommandHandlers/AI/Gemini/GeminiTextPromptHandler.cs b/BotNet.CommandHandlers/AI/Gemini/GeminiTextPromptHandler.cs index f16d604..b917b41 100644 --- a/BotNet.CommandHandlers/AI/Gemini/GeminiTextPromptHandler.cs +++ b/BotNet.CommandHandlers/AI/Gemini/GeminiTextPromptHandler.cs @@ -21,23 +21,20 @@ public sealed class GeminiTextPromptHandler( GeminiClient geminiClient, ITelegramMessageCache telegramMessageCache, CommandPriorityCategorizer commandPriorityCategorizer, - ICommandQueue commandQueue, ILogger logger ) : ICommandHandler { - internal static readonly RateLimiter CHAT_RATE_LIMITER = RateLimiter.PerUserPerChat(5, TimeSpan.FromMinutes(5)); - internal static readonly RateLimiter VIP_CHAT_RATE_LIMITER = RateLimiter.PerUserPerChat(5, TimeSpan.FromMinutes(2)); + private static readonly RateLimiter ChatRateLimiter = RateLimiter.PerUserPerChat(5, TimeSpan.FromMinutes(5)); + private static readonly RateLimiter VipChatRateLimiter = RateLimiter.PerUserPerChat(5, TimeSpan.FromMinutes(2)); private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly GeminiClient _geminiClient = geminiClient; - private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - private readonly CommandPriorityCategorizer _commandPriorityCategorizer = commandPriorityCategorizer; - private readonly ICommandQueue _commandQueue = commandQueue; - private readonly ILogger _logger = logger; - public Task Handle(GeminiTextPrompt textPrompt, CancellationToken cancellationToken) { + public Task Handle( + GeminiTextPrompt textPrompt, + CancellationToken cancellationToken + ) { if (textPrompt.Command.Chat is GroupChat) { try { - AIRateLimiters.GROUP_CHAT_RATE_LIMITER.ValidateActionRate( + AiRateLimiters.GroupChatRateLimiter.ValidateActionRate( chatId: textPrompt.Command.Chat.Id, userId: textPrompt.Command.Sender.Id ); @@ -46,9 +43,7 @@ public Task Handle(GeminiTextPrompt textPrompt, CancellationToken cancellationTo chatId: textPrompt.Command.Chat.Id, text: $"Anda terlalu banyak memanggil AI. Coba lagi {exc.Cooldown} atau lanjutkan di private chat.", parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = textPrompt.Command.MessageId - }, + replyParameters: new ReplyParameters { MessageId = textPrompt.Command.MessageId }, replyMarkup: new InlineKeyboardMarkup( InlineKeyboardButton.WithUrl("Private chat 💬", "t.me/TeknumBot") ), @@ -57,13 +52,13 @@ public Task Handle(GeminiTextPrompt textPrompt, CancellationToken cancellationTo } } else { try { - if (textPrompt.Command.Sender is VIPSender) { - VIP_CHAT_RATE_LIMITER.ValidateActionRate( + if (textPrompt.Command.Sender is VipSender) { + VipChatRateLimiter.ValidateActionRate( chatId: textPrompt.Command.Chat.Id, userId: textPrompt.Command.Sender.Id ); } else { - CHAT_RATE_LIMITER.ValidateActionRate( + ChatRateLimiter.ValidateActionRate( chatId: textPrompt.Command.Chat.Id, userId: textPrompt.Command.Sender.Id ); @@ -73,101 +68,102 @@ public Task Handle(GeminiTextPrompt textPrompt, CancellationToken cancellationTo chatId: textPrompt.Command.Chat.Id, text: $"Anda terlalu banyak memanggil AI. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = textPrompt.Command.MessageId - }, + replyParameters: new ReplyParameters { MessageId = textPrompt.Command.MessageId }, cancellationToken: cancellationToken ); } } // Fire and forget - Task.Run(async () => { - List messages = [ - Content.FromText("user", "Act as an AI assistant. The assistant is helpful, creative, direct, concise, and always get to the point."), - Content.FromText("model", "Sure.") - ]; + // ReSharper disable once MethodSupportsCancellation + Task.Run( + async () => { + List messages = [ + Content.FromText("user", "Act as an AI assistant. The assistant is helpful, creative, direct, concise, and always get to the point."), + Content.FromText("model", "Sure.") + ]; - // Merge adjacent messages from same role - foreach (MessageBase message in textPrompt.Thread.Reverse()) { - Content content = Content.FromText( - role: message.Sender.GeminiRole, - text: message.Text - ); + // Merge adjacent messages from same role + foreach (MessageBase message in textPrompt.Thread.Reverse()) { + Content content = Content.FromText( + role: message.Sender.GeminiRole, + text: message.Text + ); - if (messages.Count > 0 - && messages[^1].Role == message.Sender.GeminiRole) { - messages[^1].Add(content); - } else { - messages.Add(content); + if (messages.Count > 0 && + messages[^1].Role == message.Sender.GeminiRole) { + messages[^1] + .Add(content); + } else { + messages.Add(content); + } } - } - // Trim thread longer than 10 messages - while (messages.Count > 10) { - messages.RemoveAt(0); - } - - // Thread must start with user message - while (messages.Count > 0 - && messages[0].Role != "user") { - messages.RemoveAt(0); - } + // Trim thread longer than 10 messages + while (messages.Count > 10) { + messages.RemoveAt(0); + } - // Merge user message with replied to message if thread is initiated by replying to another user - if (messages.Count > 0 - && messages[^1].Role == "user") { - messages[^1].Add(Content.FromText("user", textPrompt.Prompt)); - } else { - messages.Add(Content.FromText("user", textPrompt.Prompt)); - } + // Thread must start with user message + while (messages.Count > 0 && + messages[0].Role != "user") { + messages.RemoveAt(0); + } - string response = await _geminiClient.ChatAsync( - messages: messages, - maxTokens: 512, - cancellationToken: cancellationToken - ); + // Merge user message with replied to message if thread is initiated by replying to another user + if (messages.Count > 0 && + messages[^1].Role == "user") { + messages[^1] + .Add(Content.FromText("user", textPrompt.Prompt)); + } else { + messages.Add(Content.FromText("user", textPrompt.Prompt)); + } - // Send response - Message responseMessage; - try { - responseMessage = await telegramBotClient.SendTextMessageAsync( - chatId: textPrompt.Command.Chat.Id, - text: response, - parseModes: [ParseMode.MarkdownV2, ParseMode.Markdown, ParseMode.Html], - replyToMessageId: textPrompt.Command.MessageId, - replyMarkup: new InlineKeyboardMarkup( - InlineKeyboardButton.WithUrl( - text: "Generated by Google Gemini 1.5 Flash", - url: "https://deepmind.google/technologies/gemini/" - ) - ), + string response = await geminiClient.ChatAsync( + messages: messages, + maxTokens: 512, cancellationToken: cancellationToken ); - } catch (Exception exc) { - _logger.LogError(exc, null); - await telegramBotClient.SendMessage( - chatId: textPrompt.Command.Chat.Id, - text: "😵", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = textPrompt.Command.MessageId - }, - cancellationToken: cancellationToken + + // Send response + Message responseMessage; + try { + responseMessage = await telegramBotClient.SendTextMessageAsync( + chatId: textPrompt.Command.Chat.Id, + text: response, + parseModes: [ParseMode.MarkdownV2, ParseMode.Markdown, ParseMode.Html], + replyToMessageId: textPrompt.Command.MessageId, + replyMarkup: new InlineKeyboardMarkup( + InlineKeyboardButton.WithUrl( + text: "Generated by Google Gemini 1.5 Flash", + url: "https://deepmind.google/technologies/gemini/" + ) + ), + cancellationToken: cancellationToken + ); + } catch (Exception exc) { + logger.LogError(exc, null); + await telegramBotClient.SendMessage( + chatId: textPrompt.Command.Chat.Id, + text: "😵", + parseMode: ParseMode.Html, + replyParameters: new ReplyParameters { MessageId = textPrompt.Command.MessageId }, + cancellationToken: cancellationToken + ); + return; + } + + // Track thread + telegramMessageCache.Add( + message: AiResponseMessage.FromMessage( + message: responseMessage, + replyToMessage: textPrompt.Command, + callSign: "Gemini", + commandPriorityCategorizer: commandPriorityCategorizer + ) ); - return; } - - // Track thread - _telegramMessageCache.Add( - message: AIResponseMessage.FromMessage( - message: responseMessage, - replyToMessage: textPrompt.Command, - callSign: "Gemini", - commandPriorityCategorizer: _commandPriorityCategorizer - ) - ); - }); + ); return Task.CompletedTask; } diff --git a/BotNet.CommandHandlers/AI/OpenAI/AskCommandHandler.cs b/BotNet.CommandHandlers/AI/OpenAI/AskCommandHandler.cs index 291f497..70a3938 100644 --- a/BotNet.CommandHandlers/AI/OpenAI/AskCommandHandler.cs +++ b/BotNet.CommandHandlers/AI/OpenAI/AskCommandHandler.cs @@ -19,26 +19,20 @@ namespace BotNet.CommandHandlers.AI.OpenAI { public sealed class AskCommandHandler( ITelegramBotClient telegramBotClient, - OpenAIClient openAIClient, + OpenAiClient openAiClient, ITelegramMessageCache telegramMessageCache, CommandPriorityCategorizer commandPriorityCategorizer, ILogger logger ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly OpenAIClient _openAIClient = openAIClient; - private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - private readonly CommandPriorityCategorizer _commandPriorityCategorizer = commandPriorityCategorizer; - private readonly ILogger _logger = logger; - public async Task Handle(AskCommand askCommand, CancellationToken cancellationToken) { if (askCommand.Command.Chat is GroupChat) { try { - AIRateLimiters.GROUP_CHAT_RATE_LIMITER.ValidateActionRate( + AiRateLimiters.GroupChatRateLimiter.ValidateActionRate( chatId: askCommand.Command.Chat.Id, userId: askCommand.Command.Sender.Id ); } catch (RateLimitExceededException exc) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: askCommand.Command.Chat.Id, text: $"Anda terlalu banyak memanggil AI. Coba lagi {exc.Cooldown} atau lanjutkan di private chat.", parseMode: ParseMode.Html, @@ -53,12 +47,12 @@ await _telegramBotClient.SendMessage( } } else { try { - OpenAITextPromptHandler.CHAT_RATE_LIMITER.ValidateActionRate( + OpenAiTextPromptHandler.ChatRateLimiter.ValidateActionRate( chatId: askCommand.Command.Chat.Id, userId: askCommand.Command.Sender.Id ); } catch (RateLimitExceededException exc) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: askCommand.Command.Chat.Id, text: $"Anda terlalu banyak memanggil AI. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, @@ -81,12 +75,12 @@ await _telegramBotClient.SendMessage( messages.AddRange( from message in askCommand.Thread.Take(10).Reverse() select ChatMessage.FromText( - role: message.Sender.ChatGPTRole, + role: message.Sender.ChatGptRole, text: message.Text ) ); - Message responseMessage = await _telegramBotClient.SendMessage( + Message responseMessage = await telegramBotClient.SendMessage( chatId: askCommand.Command.Chat.Id, text: MarkdownV2Sanitizer.Sanitize("… ⏳"), parseMode: ParseMode.MarkdownV2, @@ -95,9 +89,9 @@ select ChatMessage.FromText( } ); - string response = await _openAIClient.ChatAsync( + string response = await openAiClient.ChatAsync( model: askCommand switch { - ({ Command: { Sender: VIPSender } or { Chat: HomeGroupChat } }) => "gpt-4-1106-preview", + ({ Command: { Sender: VipSender } or { Chat: HomeGroupChat } }) => "gpt-4-1106-preview", _ => "gpt-3.5-turbo" }, messages: messages, @@ -115,7 +109,7 @@ select ChatMessage.FromText( replyMarkup: new InlineKeyboardMarkup( InlineKeyboardButton.WithUrl( text: askCommand switch { - ({ Command: { Sender: VIPSender } or { Chat: HomeGroupChat } }) => "Generated by OpenAI GPT-4", + ({ Command: { Sender: VipSender } or { Chat: HomeGroupChat } }) => "Generated by OpenAI GPT-4", _ => "Generated by OpenAI GPT-3.5 Turbo" }, url: "https://openai.com/gpt-4" @@ -124,7 +118,7 @@ select ChatMessage.FromText( cancellationToken: cancellationToken ); } catch (Exception exc) { - _logger.LogError(exc, null); + logger.LogError(exc, null); await telegramBotClient.EditMessageText( chatId: askCommand.Command.Chat.Id, messageId: responseMessage.MessageId, @@ -136,12 +130,12 @@ await telegramBotClient.EditMessageText( } // Track thread - _telegramMessageCache.Add( - message: AIResponseMessage.FromMessage( + telegramMessageCache.Add( + message: AiResponseMessage.FromMessage( message: responseMessage, replyToMessage: askCommand.Command, callSign: "GPT", - commandPriorityCategorizer: _commandPriorityCategorizer + commandPriorityCategorizer: commandPriorityCategorizer ) ); }); diff --git a/BotNet.CommandHandlers/AI/OpenAI/OpenAIImageGenerationPromptHandler.cs b/BotNet.CommandHandlers/AI/OpenAI/OpenAIImageGenerationPromptHandler.cs index 4602c92..aacd96b 100644 --- a/BotNet.CommandHandlers/AI/OpenAI/OpenAIImageGenerationPromptHandler.cs +++ b/BotNet.CommandHandlers/AI/OpenAI/OpenAIImageGenerationPromptHandler.cs @@ -10,32 +10,26 @@ using Telegram.Bot.Types.ReplyMarkups; namespace BotNet.CommandHandlers.AI.OpenAI { - public sealed class OpenAIImageGenerationPromptHandler( + public sealed class OpenAiImageGenerationPromptHandler( ITelegramBotClient telegramBotClient, ImageGenerationBot imageGenerationBot, ITelegramMessageCache telegramMessageCache, CommandPriorityCategorizer commandPriorityCategorizer, - ILogger logger - ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly ImageGenerationBot _imageGenerationBot = imageGenerationBot; - private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - private readonly CommandPriorityCategorizer _commandPriorityCategorizer = commandPriorityCategorizer; - private readonly ILogger _logger = logger; - - public Task Handle(OpenAIImageGenerationPrompt command, CancellationToken cancellationToken) { + ILogger logger + ) : ICommandHandler { + public Task Handle(OpenAiImageGenerationPrompt command, CancellationToken cancellationToken) { // Fire and forget Task.Run(async () => { try { Uri generatedImageUrl; try { - generatedImageUrl = await _imageGenerationBot.GenerateImageAsync( + generatedImageUrl = await imageGenerationBot.GenerateImageAsync( prompt: command.Prompt, cancellationToken: cancellationToken ); } catch (Exception exc) { - _logger.LogError(exc, "Could not generate image"); - await _telegramBotClient.EditMessageText( + logger.LogError(exc, "Could not generate image"); + await telegramBotClient.EditMessageText( chatId: command.Chat.Id, messageId: command.ResponseMessageId, text: "Failed to generate image.", @@ -47,7 +41,7 @@ await _telegramBotClient.EditMessageText( // Delete busy message try { - await _telegramBotClient.DeleteMessage( + await telegramBotClient.DeleteMessage( chatId: command.Chat.Id, messageId: command.ResponseMessageId, cancellationToken: cancellationToken @@ -57,7 +51,7 @@ await _telegramBotClient.DeleteMessage( } // Send generated image - Message responseMessage = await _telegramBotClient.SendPhoto( + Message responseMessage = await telegramBotClient.SendPhoto( chatId: command.Chat.Id, photo: new InputFileUrl(generatedImageUrl), replyMarkup: new InlineKeyboardMarkup( @@ -73,14 +67,14 @@ await _telegramBotClient.DeleteMessage( ); // Track thread - _telegramMessageCache.Add( - NormalMessage.FromMessage(responseMessage, _commandPriorityCategorizer) + telegramMessageCache.Add( + NormalMessage.FromMessage(responseMessage, commandPriorityCategorizer) ); } catch (OperationCanceledException) { // Terminate gracefully // TODO: tie up loose ends } catch (Exception exc) { - _logger.LogError(exc, "Could not handle command"); + logger.LogError(exc, "Could not handle command"); } }); diff --git a/BotNet.CommandHandlers/AI/OpenAI/OpenAIImagePromptHandler.cs b/BotNet.CommandHandlers/AI/OpenAI/OpenAIImagePromptHandler.cs index bdbfbc3..aabff6e 100644 --- a/BotNet.CommandHandlers/AI/OpenAI/OpenAIImagePromptHandler.cs +++ b/BotNet.CommandHandlers/AI/OpenAI/OpenAIImagePromptHandler.cs @@ -19,28 +19,21 @@ using Telegram.Bot.Types.ReplyMarkups; namespace BotNet.CommandHandlers.AI.OpenAI { - public sealed class OpenAIImagePromptHandler( + public sealed class OpenAiImagePromptHandler( ITelegramBotClient telegramBotClient, ICommandQueue commandQueue, ITelegramMessageCache telegramMessageCache, - OpenAIClient openAIClient, + OpenAiClient openAiClient, CommandPriorityCategorizer commandPriorityCategorizer, - ILogger logger - ) : ICommandHandler { - internal static readonly RateLimiter VISION_RATE_LIMITER = RateLimiter.PerUserPerChat(1, TimeSpan.FromMinutes(15)); - internal static readonly RateLimiter VIP_VISION_RATE_LIMITER = RateLimiter.PerUserPerChat(2, TimeSpan.FromMinutes(2)); + ILogger logger + ) : ICommandHandler { + private static readonly RateLimiter VisionRateLimiter = RateLimiter.PerUserPerChat(1, TimeSpan.FromMinutes(15)); + private static readonly RateLimiter VipVisionRateLimiter = RateLimiter.PerUserPerChat(2, TimeSpan.FromMinutes(2)); - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly ICommandQueue _commandQueue = commandQueue; - private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - private readonly OpenAIClient _openAIClient = openAIClient; - private readonly CommandPriorityCategorizer _commandPriorityCategorizer = commandPriorityCategorizer; - private readonly ILogger _logger = logger; - - public Task Handle(OpenAIImagePrompt imagePrompt, CancellationToken cancellationToken) { - if (imagePrompt.Command.Sender is not VIPSender + public Task Handle(OpenAiImagePrompt imagePrompt, CancellationToken cancellationToken) { + if (imagePrompt.Command.Sender is not VipSender && imagePrompt.Command.Chat is not HomeGroupChat) { - return _telegramBotClient.SendMessage( + return telegramBotClient.SendMessage( chatId: imagePrompt.Command.Chat.Id, text: MarkdownV2Sanitizer.Sanitize("Vision tidak bisa dipakai di sini."), parseMode: ParseMode.MarkdownV2, @@ -52,19 +45,19 @@ public Task Handle(OpenAIImagePrompt imagePrompt, CancellationToken cancellation } try { - if (imagePrompt.Command.Sender is VIPSender) { - VIP_VISION_RATE_LIMITER.ValidateActionRate( + if (imagePrompt.Command.Sender is VipSender) { + VipVisionRateLimiter.ValidateActionRate( chatId: imagePrompt.Command.Chat.Id, userId: imagePrompt.Command.Sender.Id ); } else { - VISION_RATE_LIMITER.ValidateActionRate( + VisionRateLimiter.ValidateActionRate( chatId: imagePrompt.Command.Chat.Id, userId: imagePrompt.Command.Sender.Id ); } } catch (RateLimitExceededException exc) { - return _telegramBotClient.SendMessage( + return telegramBotClient.SendMessage( chatId: imagePrompt.Command.Chat.Id, text: $"Anda terlalu banyak menggunakan vision. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, @@ -78,13 +71,13 @@ public Task Handle(OpenAIImagePrompt imagePrompt, CancellationToken cancellation // Fire and forget Task.Run(async () => { (string? imageBase64, string? error) = await GetImageBase64Async( - botClient: _telegramBotClient, + botClient: telegramBotClient, fileId: imagePrompt.ImageFileId, cancellationToken: cancellationToken ); if (error is not null) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: imagePrompt.Command.Chat.Id, text: $"{error}", parseMode: ParseMode.Html, @@ -103,7 +96,7 @@ await _telegramBotClient.SendMessage( messages.AddRange( from message in imagePrompt.Thread.Take(10).Reverse() select ChatMessage.FromText( - role: message.Sender.ChatGPTRole, + role: message.Sender.ChatGptRole, text: message.Text ) ); @@ -112,7 +105,7 @@ select ChatMessage.FromText( ChatMessage.FromTextWithImageBase64("user", imagePrompt.Prompt, imageBase64!) ); - Message responseMessage = await _telegramBotClient.SendMessage( + Message responseMessage = await telegramBotClient.SendMessage( chatId: imagePrompt.Command.Chat.Id, text: MarkdownV2Sanitizer.Sanitize("… ⏳"), parseMode: ParseMode.MarkdownV2, @@ -121,7 +114,7 @@ select ChatMessage.FromText( } ); - string response = await _openAIClient.ChatAsync( + string response = await openAiClient.ChatAsync( model: "gpt-4-vision-preview", messages: messages, maxTokens: 512, @@ -130,11 +123,11 @@ select ChatMessage.FromText( // Handle image generation intent if (response.StartsWith("ImageGeneration:")) { - if (imagePrompt.Command.Sender is not VIPSender) { + if (imagePrompt.Command.Sender is not VipSender) { try { - ArtCommandHandler.IMAGE_GENERATION_RATE_LIMITER.ValidateActionRate(imagePrompt.Command.Chat.Id, imagePrompt.Command.Sender.Id); + ArtCommandHandler.ImageGenerationRateLimiter.ValidateActionRate(imagePrompt.Command.Chat.Id, imagePrompt.Command.Sender.Id); } catch (RateLimitExceededException exc) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: imagePrompt.Command.Chat.Id, text: $"Anda belum mendapat giliran. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, @@ -149,9 +142,9 @@ await _telegramBotClient.SendMessage( string imageGenerationPrompt = response.Substring(response.IndexOf(':') + 1).Trim(); switch (imagePrompt.Command) { - case { Sender: VIPSender }: - await _commandQueue.DispatchAsync( - command: new OpenAIImageGenerationPrompt( + case { Sender: VipSender }: + await commandQueue.DispatchAsync( + command: new OpenAiImageGenerationPrompt( callSign: imagePrompt.CallSign, prompt: imageGenerationPrompt, promptMessageId: imagePrompt.Command.MessageId, @@ -162,7 +155,7 @@ await _commandQueue.DispatchAsync( ); break; case { Chat: HomeGroupChat }: - await _commandQueue.DispatchAsync( + await commandQueue.DispatchAsync( command: new StabilityTextToImagePrompt( callSign: imagePrompt.CallSign, prompt: imageGenerationPrompt, @@ -174,7 +167,7 @@ await _commandQueue.DispatchAsync( ); break; default: - await _telegramBotClient.EditMessageText( + await telegramBotClient.EditMessageText( chatId: imagePrompt.Command.Chat.Id, messageId: responseMessage.MessageId, text: MarkdownV2Sanitizer.Sanitize("Image generation tidak bisa dipakai di sini."), @@ -202,7 +195,7 @@ await _telegramBotClient.EditMessageText( cancellationToken: cancellationToken ); } catch (Exception exc) { - _logger.LogError(exc, null); + logger.LogError(exc, null); await telegramBotClient.EditMessageText( chatId: imagePrompt.Command.Chat.Id, messageId: responseMessage.MessageId, @@ -214,12 +207,12 @@ await telegramBotClient.EditMessageText( } // Track thread - _telegramMessageCache.Add( - message: AIResponseMessage.FromMessage( + telegramMessageCache.Add( + message: AiResponseMessage.FromMessage( message: responseMessage, replyToMessage: imagePrompt.Command, callSign: imagePrompt.CallSign, - commandPriorityCategorizer: _commandPriorityCategorizer + commandPriorityCategorizer: commandPriorityCategorizer ) ); }); diff --git a/BotNet.CommandHandlers/AI/OpenAI/OpenAITextPromptHandler.cs b/BotNet.CommandHandlers/AI/OpenAI/OpenAITextPromptHandler.cs index 8a64659..995edf5 100644 --- a/BotNet.CommandHandlers/AI/OpenAI/OpenAITextPromptHandler.cs +++ b/BotNet.CommandHandlers/AI/OpenAI/OpenAITextPromptHandler.cs @@ -19,32 +19,25 @@ using Telegram.Bot.Types.ReplyMarkups; namespace BotNet.CommandHandlers.AI.OpenAI { - public sealed class OpenAITextPromptHandler( + public sealed class OpenAiTextPromptHandler( ITelegramBotClient telegramBotClient, ICommandQueue commandQueue, ITelegramMessageCache telegramMessageCache, - OpenAIClient openAIClient, + OpenAiClient openAiClient, CommandPriorityCategorizer commandPriorityCategorizer, - ILogger logger - ) : ICommandHandler { - internal static readonly RateLimiter CHAT_RATE_LIMITER = RateLimiter.PerUserPerChat(5, TimeSpan.FromMinutes(15)); + ILogger logger + ) : ICommandHandler { + internal static readonly RateLimiter ChatRateLimiter = RateLimiter.PerUserPerChat(5, TimeSpan.FromMinutes(15)); - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly ICommandQueue _commandQueue = commandQueue; - private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - private readonly OpenAIClient _openAIClient = openAIClient; - private readonly CommandPriorityCategorizer _commandPriorityCategorizer = commandPriorityCategorizer; - private readonly ILogger _logger = logger; - - public Task Handle(OpenAITextPrompt textPrompt, CancellationToken cancellationToken) { + public Task Handle(OpenAiTextPrompt textPrompt, CancellationToken cancellationToken) { if (textPrompt.Command.Chat is GroupChat) { try { - AIRateLimiters.GROUP_CHAT_RATE_LIMITER.ValidateActionRate( + AiRateLimiters.GroupChatRateLimiter.ValidateActionRate( chatId: textPrompt.Command.Chat.Id, userId: textPrompt.Command.Sender.Id ); } catch (RateLimitExceededException exc) { - return _telegramBotClient.SendMessage( + return telegramBotClient.SendMessage( chatId: textPrompt.Command.Chat.Id, text: $"Anda terlalu banyak memanggil AI. Coba lagi {exc.Cooldown} atau lanjutkan di private chat.", parseMode: ParseMode.Html, @@ -59,12 +52,12 @@ public Task Handle(OpenAITextPrompt textPrompt, CancellationToken cancellationTo } } else { try { - CHAT_RATE_LIMITER.ValidateActionRate( + ChatRateLimiter.ValidateActionRate( chatId: textPrompt.Command.Chat.Id, userId: textPrompt.Command.Sender.Id ); } catch (RateLimitExceededException exc) { - return _telegramBotClient.SendMessage( + return telegramBotClient.SendMessage( chatId: textPrompt.Command.Chat.Id, text: $"Anda terlalu banyak memanggil AI. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, @@ -85,7 +78,7 @@ public Task Handle(OpenAITextPrompt textPrompt, CancellationToken cancellationTo messages.AddRange( from message in textPrompt.Thread.Take(10).Reverse() select ChatMessage.FromText( - role: message.Sender.ChatGPTRole, + role: message.Sender.ChatGptRole, text: message.Text ) ); @@ -94,7 +87,7 @@ select ChatMessage.FromText( ChatMessage.FromText("user", textPrompt.Prompt) ); - Message responseMessage = await _telegramBotClient.SendMessage( + Message responseMessage = await telegramBotClient.SendMessage( chatId: textPrompt.Command.Chat.Id, text: MarkdownV2Sanitizer.Sanitize("… ⏳"), parseMode: ParseMode.MarkdownV2, @@ -103,9 +96,9 @@ select ChatMessage.FromText( } ); - string response = await _openAIClient.ChatAsync( + string response = await openAiClient.ChatAsync( model: textPrompt switch { - ({ Command: { Sender: VIPSender } or { Chat: HomeGroupChat } }) => "gpt-4-1106-preview", + { Command: { Sender: VipSender } or { Chat: HomeGroupChat } } => "gpt-4-1106-preview", _ => "gpt-3.5-turbo" }, messages: messages, @@ -115,11 +108,11 @@ select ChatMessage.FromText( // Handle image generation intent if (response.StartsWith("ImageGeneration:")) { - if (textPrompt.Command.Sender is not VIPSender) { + if (textPrompt.Command.Sender is not VipSender) { try { - ArtCommandHandler.IMAGE_GENERATION_RATE_LIMITER.ValidateActionRate(textPrompt.Command.Chat.Id, textPrompt.Command.Sender.Id); + ArtCommandHandler.ImageGenerationRateLimiter.ValidateActionRate(textPrompt.Command.Chat.Id, textPrompt.Command.Sender.Id); } catch (RateLimitExceededException exc) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: textPrompt.Command.Chat.Id, text: $"Anda belum mendapat giliran. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, @@ -134,9 +127,9 @@ await _telegramBotClient.SendMessage( string imageGenerationPrompt = response.Substring(response.IndexOf(':') + 1).Trim(); switch (textPrompt.Command) { - case { Sender: VIPSender }: - await _commandQueue.DispatchAsync( - command: new OpenAIImageGenerationPrompt( + case { Sender: VipSender }: + await commandQueue.DispatchAsync( + command: new OpenAiImageGenerationPrompt( callSign: textPrompt.CallSign, prompt: imageGenerationPrompt, promptMessageId: textPrompt.Command.MessageId, @@ -147,7 +140,7 @@ await _commandQueue.DispatchAsync( ); break; case { Chat: HomeGroupChat }: - await _commandQueue.DispatchAsync( + await commandQueue.DispatchAsync( command: new StabilityTextToImagePrompt( callSign: textPrompt.CallSign, prompt: imageGenerationPrompt, @@ -159,7 +152,7 @@ await _commandQueue.DispatchAsync( ); break; default: - await _telegramBotClient.EditMessageText( + await telegramBotClient.EditMessageText( chatId: textPrompt.Command.Chat.Id, messageId: responseMessage.MessageId, text: MarkdownV2Sanitizer.Sanitize("Image generation tidak bisa dipakai di sini."), @@ -181,7 +174,7 @@ await _telegramBotClient.EditMessageText( replyMarkup: new InlineKeyboardMarkup( InlineKeyboardButton.WithUrl( text: textPrompt switch { - ({ Command: { Sender: VIPSender } or { Chat: HomeGroupChat } }) => "Generated by OpenAI GPT-4", + { Command: { Sender: VipSender } or { Chat: HomeGroupChat } } => "Generated by OpenAI GPT-4", _ => "Generated by OpenAI GPT-3.5 Turbo" }, url: "https://openai.com/gpt-4" @@ -190,7 +183,7 @@ await _telegramBotClient.EditMessageText( cancellationToken: cancellationToken ); } catch (Exception exc) { - _logger.LogError(exc, null); + logger.LogError(exc, null); await telegramBotClient.EditMessageText( chatId: textPrompt.Command.Chat.Id, messageId: responseMessage.MessageId, @@ -202,12 +195,12 @@ await telegramBotClient.EditMessageText( } // Track thread - _telegramMessageCache.Add( - message: AIResponseMessage.FromMessage( + telegramMessageCache.Add( + message: AiResponseMessage.FromMessage( message: responseMessage, replyToMessage: textPrompt.Command, callSign: textPrompt.CallSign, - commandPriorityCategorizer: _commandPriorityCategorizer + commandPriorityCategorizer: commandPriorityCategorizer ) ); }); diff --git a/BotNet.CommandHandlers/AI/RateLimit/AIRateLimiters.cs b/BotNet.CommandHandlers/AI/RateLimit/AIRateLimiters.cs index 22e6f7c..f8c1358 100644 --- a/BotNet.CommandHandlers/AI/RateLimit/AIRateLimiters.cs +++ b/BotNet.CommandHandlers/AI/RateLimit/AIRateLimiters.cs @@ -1,7 +1,7 @@ using BotNet.Services.RateLimit; namespace BotNet.CommandHandlers.AI.RateLimit { - internal static class AIRateLimiters { - internal static readonly RateLimiter GROUP_CHAT_RATE_LIMITER = RateLimiter.PerUserPerChat(4, TimeSpan.FromMinutes(60)); + internal static class AiRateLimiters { + internal static readonly RateLimiter GroupChatRateLimiter = RateLimiter.PerUserPerChat(4, TimeSpan.FromMinutes(60)); } } diff --git a/BotNet.CommandHandlers/AI/Stability/StabilityTextToImagePromptHandler.cs b/BotNet.CommandHandlers/AI/Stability/StabilityTextToImagePromptHandler.cs index 002446d..7a1bd57 100644 --- a/BotNet.CommandHandlers/AI/Stability/StabilityTextToImagePromptHandler.cs +++ b/BotNet.CommandHandlers/AI/Stability/StabilityTextToImagePromptHandler.cs @@ -16,32 +16,27 @@ public sealed class StabilityTextToImagePromptHandler( ITelegramMessageCache telegramMessageCache, CommandPriorityCategorizer commandPriorityCategorizer ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly ImageGenerationBot _imageGenerationBot = imageGenerationBot; - private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - private readonly CommandPriorityCategorizer _commandPriorityCategorizer = commandPriorityCategorizer; - public Task Handle(StabilityTextToImagePrompt command, CancellationToken cancellationToken) { // Fire and forget Task.Run(async () => { try { byte[] generatedImage; try { - generatedImage = await _imageGenerationBot.GenerateImageAsync( + generatedImage = await imageGenerationBot.GenerateImageAsync( prompt: command.Prompt, cancellationToken: cancellationToken ); } catch (ContentFilteredException exc) { - await _telegramBotClient.EditMessageText( + await telegramBotClient.EditMessageText( chatId: command.Chat.Id, messageId: command.ResponseMessageId, - text: $"{exc.Message ?? "Content filtered."}", + text: $"{exc.Message}", parseMode: ParseMode.Html, cancellationToken: cancellationToken ); return; } catch { - await _telegramBotClient.EditMessageText( + await telegramBotClient.EditMessageText( chatId: command.Chat.Id, messageId: command.ResponseMessageId, text: "Failed to generate image.", @@ -53,7 +48,7 @@ await _telegramBotClient.EditMessageText( // Delete busy message try { - await _telegramBotClient.DeleteMessage( + await telegramBotClient.DeleteMessage( chatId: command.Chat.Id, messageId: command.ResponseMessageId, cancellationToken: cancellationToken @@ -64,7 +59,7 @@ await _telegramBotClient.DeleteMessage( // Send generated image using MemoryStream generatedImageStream = new(generatedImage); - Message responseMessage = await _telegramBotClient.SendPhoto( + Message responseMessage = await telegramBotClient.SendPhoto( chatId: command.Chat.Id, photo: new InputFileStream(generatedImageStream, "art.png"), replyMarkup: new InlineKeyboardMarkup( @@ -80,8 +75,8 @@ await _telegramBotClient.DeleteMessage( ); // Track thread - _telegramMessageCache.Add( - NormalMessage.FromMessage(responseMessage, _commandPriorityCategorizer) + telegramMessageCache.Add( + NormalMessage.FromMessage(responseMessage, commandPriorityCategorizer) ); } catch (OperationCanceledException) { // Terminate gracefully diff --git a/BotNet.CommandHandlers/Art/ArtCommandHandler.cs b/BotNet.CommandHandlers/Art/ArtCommandHandler.cs index cd96deb..2e8ddc2 100644 --- a/BotNet.CommandHandlers/Art/ArtCommandHandler.cs +++ b/BotNet.CommandHandlers/Art/ArtCommandHandler.cs @@ -15,16 +15,13 @@ public sealed class ArtCommandHandler( ITelegramBotClient telegramBotClient, ICommandQueue commandQueue ) : ICommandHandler { - internal static readonly RateLimiter IMAGE_GENERATION_RATE_LIMITER = RateLimiter.PerUser(2, TimeSpan.FromMinutes(3)); - - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly ICommandQueue _commandQueue = commandQueue; + internal static readonly RateLimiter ImageGenerationRateLimiter = RateLimiter.PerUser(2, TimeSpan.FromMinutes(3)); public Task Handle(ArtCommand command, CancellationToken cancellationToken) { try { - IMAGE_GENERATION_RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); + ImageGenerationRateLimiter.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException exc) { - return _telegramBotClient.SendMessage( + return telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $"Anda belum mendapat giliran. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, @@ -39,8 +36,8 @@ public Task Handle(ArtCommand command, CancellationToken cancellationToken) { Task.Run(async () => { try { switch (command) { - case { Sender: VIPSender }: { - Message busyMessage = await _telegramBotClient.SendMessage( + case { Sender: VipSender }: { + Message busyMessage = await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Generating image… ⏳", parseMode: ParseMode.Markdown, @@ -50,8 +47,8 @@ public Task Handle(ArtCommand command, CancellationToken cancellationToken) { cancellationToken: cancellationToken ); - await _commandQueue.DispatchAsync( - new OpenAIImageGenerationPrompt( + await commandQueue.DispatchAsync( + new OpenAiImageGenerationPrompt( callSign: "GPT", prompt: command.Prompt, promptMessageId: command.PromptMessageId, @@ -63,7 +60,7 @@ await _commandQueue.DispatchAsync( } break; case { Chat: HomeGroupChat }: { - Message busyMessage = await _telegramBotClient.SendMessage( + Message busyMessage = await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Generating image… ⏳", parseMode: ParseMode.Markdown, @@ -73,7 +70,7 @@ await _commandQueue.DispatchAsync( cancellationToken: cancellationToken ); - await _commandQueue.DispatchAsync( + await commandQueue.DispatchAsync( new StabilityTextToImagePrompt( callSign: "GPT", prompt: command.Prompt, @@ -86,7 +83,7 @@ await _commandQueue.DispatchAsync( } break; default: - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: MarkdownV2Sanitizer.Sanitize("Image generation tidak bisa dipakai di sini."), parseMode: ParseMode.MarkdownV2, diff --git a/BotNet.CommandHandlers/BMKG/BMKGCommandHandler.cs b/BotNet.CommandHandlers/BMKG/BMKGCommandHandler.cs index 9b9ad30..0f78dcc 100644 --- a/BotNet.CommandHandlers/BMKG/BMKGCommandHandler.cs +++ b/BotNet.CommandHandlers/BMKG/BMKGCommandHandler.cs @@ -6,20 +6,17 @@ using Telegram.Bot.Types.Enums; namespace BotNet.CommandHandlers.BMKG { - public sealed class BMKGCommandHandler( + public sealed class BmkgCommandHandler( ITelegramBotClient telegramBotClient, LatestEarthQuake latestEarthQuake - ) : ICommandHandler { - private static readonly RateLimiter RATE_LIMITER = RateLimiter.PerChat(3, TimeSpan.FromMinutes(2)); + ) : ICommandHandler { + private static readonly RateLimiter RateLimiter = RateLimiter.PerChat(3, TimeSpan.FromMinutes(2)); - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly LatestEarthQuake _latestEarthQuake = latestEarthQuake; - - public Task Handle(BMKGCommand command, CancellationToken cancellationToken) { + public Task Handle(BmkgCommand command, CancellationToken cancellationToken) { try { - RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); + RateLimiter.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException exc) { - return _telegramBotClient.SendMessage( + return telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $"Sabar dulu ya, tunggu giliran yang lain. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, @@ -33,9 +30,9 @@ public Task Handle(BMKGCommand command, CancellationToken cancellationToken) { // Fire and forget Task.Run(async () => { try { - (string text, string shakemapUrl) = await _latestEarthQuake.GetLatestAsync(); + (string text, string shakemapUrl) = await latestEarthQuake.GetLatestAsync(); - await _telegramBotClient.SendPhoto( + await telegramBotClient.SendPhoto( chatId: command.Chat.Id, photo: new InputFileUrl(shakemapUrl), caption: text, diff --git a/BotNet.CommandHandlers/BotUpdate/CallbackQuery/CallbackQueryUpdateHandler.cs b/BotNet.CommandHandlers/BotUpdate/CallbackQuery/CallbackQueryUpdateHandler.cs index 90af78b..c4602f5 100644 --- a/BotNet.CommandHandlers/BotUpdate/CallbackQuery/CallbackQueryUpdateHandler.cs +++ b/BotNet.CommandHandlers/BotUpdate/CallbackQuery/CallbackQueryUpdateHandler.cs @@ -6,8 +6,6 @@ namespace BotNet.CommandHandlers.BotUpdate.CallbackQuery { public sealed class CallbackQueryUpdateHandler( ICommandQueue commandQueue ) : ICommandHandler { - private readonly ICommandQueue _commandQueue = commandQueue; - public async Task Handle(CallbackQueryUpdate command, CancellationToken cancellationToken) { // Only handle callback queries with data if (command.CallbackQuery.Data is not { } data) { @@ -27,7 +25,7 @@ public async Task Handle(CallbackQueryUpdate command, CancellationToken cancella callbackQuery: command.CallbackQuery, out BubbleWrapCallback? bubbleWrapCallback )) { - await _commandQueue.DispatchAsync(bubbleWrapCallback); + await commandQueue.DispatchAsync(bubbleWrapCallback); } break; } diff --git a/BotNet.CommandHandlers/BotUpdate/InlineQuery/InlineQueryUpdateHandler.cs b/BotNet.CommandHandlers/BotUpdate/InlineQuery/InlineQueryUpdateHandler.cs index 87bae9c..55c538f 100644 --- a/BotNet.CommandHandlers/BotUpdate/InlineQuery/InlineQueryUpdateHandler.cs +++ b/BotNet.CommandHandlers/BotUpdate/InlineQuery/InlineQueryUpdateHandler.cs @@ -13,16 +13,12 @@ public sealed class InlineQueryUpdateHandler( BrainfuckTranspiler brainfuckTranspiler, ILogger logger ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly BrainfuckTranspiler _brainfuckTranspiler = brainfuckTranspiler; - private readonly ILogger _logger = logger; - public Task Handle(InlineQueryUpdate command, CancellationToken cancellationToken) { // Fire and forget Task.Run(async () => { try { // Query must not be empty - if (command.InlineQuery?.Query.Trim() is not { Length: > 0 } query) { + if (command.InlineQuery.Query.Trim() is not { Length: > 0 } query) { return; } @@ -53,7 +49,7 @@ public Task Handle(InlineQueryUpdate command, CancellationToken cancellationToke } // Generate brainfuck code - string brainfuckCode = _brainfuckTranspiler.TranspileBrainfuck(query); + string brainfuckCode = brainfuckTranspiler.TranspileBrainfuck(query); results.Add(new InlineQueryResultArticle( id: Guid.NewGuid().ToString("N"), title: brainfuckCode, @@ -61,7 +57,7 @@ public Task Handle(InlineQueryUpdate command, CancellationToken cancellationToke )); // Send results - await _telegramBotClient.AnswerInlineQuery( + await telegramBotClient.AnswerInlineQuery( inlineQueryId: command.InlineQuery.Id, results: results, cancellationToken: cancellationToken @@ -69,7 +65,7 @@ await _telegramBotClient.AnswerInlineQuery( } catch (OperationCanceledException) { // Terminate gracefully } catch (Exception exc) { - _logger.LogError(exc, "Could not handle inline query"); + logger.LogError(exc, "Could not handle inline query"); } }); diff --git a/BotNet.CommandHandlers/BotUpdate/Message/AICallCommandHandler.cs b/BotNet.CommandHandlers/BotUpdate/Message/AICallCommandHandler.cs index 78577fa..5b9bb96 100644 --- a/BotNet.CommandHandlers/BotUpdate/Message/AICallCommandHandler.cs +++ b/BotNet.CommandHandlers/BotUpdate/Message/AICallCommandHandler.cs @@ -2,49 +2,43 @@ using BotNet.Commands.AI.Gemini; using BotNet.Commands.AI.OpenAI; using BotNet.Commands.BotUpdate.Message; -using BotNet.Services.OpenAI; namespace BotNet.CommandHandlers.BotUpdate.Message { - public sealed class AICallCommandHandler( + public sealed class AiCallCommandHandler( ICommandQueue commandQueue, - ITelegramMessageCache telegramMessageCache, - IntentDetector intentDetector - ) : ICommandHandler { - private readonly ICommandQueue _commandQueue = commandQueue; - private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - private readonly IntentDetector _intentDetector = intentDetector; - - public async Task Handle(AICallCommand command, CancellationToken cancellationToken) { + ITelegramMessageCache telegramMessageCache + ) : ICommandHandler { + public async Task Handle(AiCallCommand command, CancellationToken cancellationToken) { switch (command.CallSign) { case "GPT" when command.ImageFileId is null && command.ReplyToMessage?.ImageFileId is null: { - await _commandQueue.DispatchAsync( - command: OpenAITextPrompt.FromAICallCommand( + await commandQueue.DispatchAsync( + command: OpenAiTextPrompt.FromAiCallCommand( aiCallCommand: command, thread: command.ReplyToMessage is { } replyToMessage - ? _telegramMessageCache.GetThread(replyToMessage) - : Enumerable.Empty() + ? telegramMessageCache.GetThread(replyToMessage) + : [] ) ); break; } case "GPT" when command.ImageFileId is not null || command.ReplyToMessage?.ImageFileId is not null: { - await _commandQueue.DispatchAsync( - command: OpenAIImagePrompt.FromAICallCommand( + await commandQueue.DispatchAsync( + command: OpenAiImagePrompt.FromAiCallCommand( aiCallCommand: command, thread: command.ReplyToMessage is { } replyToMessage - ? _telegramMessageCache.GetThread(replyToMessage) - : Enumerable.Empty() + ? telegramMessageCache.GetThread(replyToMessage) + : [] ) ); break; } case "AI" or "Bot" or "Gemini" when command.ImageFileId is null && command.ReplyToMessage?.ImageFileId is null: { - await _commandQueue.DispatchAsync( - command: GeminiTextPrompt.FromAICallCommand( + await commandQueue.DispatchAsync( + command: GeminiTextPrompt.FromAiCallCommand( aiCallCommand: command, thread: command.ReplyToMessage is { } replyToMessage - ? _telegramMessageCache.GetThread(replyToMessage) - : Enumerable.Empty() + ? telegramMessageCache.GetThread(replyToMessage) + : [] ) ); break; diff --git a/BotNet.CommandHandlers/BotUpdate/Message/AIFollowUpMessageHandler.cs b/BotNet.CommandHandlers/BotUpdate/Message/AIFollowUpMessageHandler.cs index b05e37c..6cc1017 100644 --- a/BotNet.CommandHandlers/BotUpdate/Message/AIFollowUpMessageHandler.cs +++ b/BotNet.CommandHandlers/BotUpdate/Message/AIFollowUpMessageHandler.cs @@ -4,39 +4,32 @@ using BotNet.Commands.BotUpdate.Message; namespace BotNet.CommandHandlers.BotUpdate.Message { - public sealed class AIFollowUpMessageHandler( + public sealed class AiFollowUpMessageHandler( ICommandQueue commandQueue, ITelegramMessageCache telegramMessageCache - ) : ICommandHandler { - private readonly ICommandQueue _commandQueue = commandQueue; - private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - - public async Task Handle(AIFollowUpMessage command, CancellationToken cancellationToken) { + ) : ICommandHandler { + public async Task Handle(AiFollowUpMessage command, CancellationToken cancellationToken) { switch (command.CallSign) { // OpenAI GPT-4 Chat case "GPT": - await _commandQueue.DispatchAsync( - command: OpenAITextPrompt.FromAIFollowUpMessage( + await commandQueue.DispatchAsync( + command: OpenAiTextPrompt.FromAiFollowUpMessage( aiFollowUpMessage: command, - thread: command.ReplyToMessage is null - ? Enumerable.Empty() - : _telegramMessageCache.GetThread( - firstMessage: command.ReplyToMessage - ) + thread: telegramMessageCache.GetThread( + firstMessage: command.ReplyToMessage + ) ) ); break; // Google Gemini Chat case "AI" or "Bot" or "Gemini": - await _commandQueue.DispatchAsync( - command: GeminiTextPrompt.FromAIFollowUpMessage( + await commandQueue.DispatchAsync( + command: GeminiTextPrompt.FromAiFollowUpMessage( aIFollowUpMessage: command, - thread: command.ReplyToMessage is null - ? Enumerable.Empty() - : _telegramMessageCache.GetThread( - firstMessage: command.ReplyToMessage - ) + thread: telegramMessageCache.GetThread( + firstMessage: command.ReplyToMessage + ) ) ); break; diff --git a/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs b/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs index 8f3f95d..359fa2a 100644 --- a/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs +++ b/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs @@ -19,127 +19,129 @@ public sealed class MessageUpdateHandler( BotProfileAccessor botProfileAccessor, CommandPriorityCategorizer commandPriorityCategorizer ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly ICommandQueue _commandQueue = commandQueue; - private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - private readonly BotProfileAccessor _botProfileAccessor = botProfileAccessor; - private readonly CommandPriorityCategorizer _commandPriorityCategorizer = commandPriorityCategorizer; - - public async Task Handle(MessageUpdate update, CancellationToken cancellationToken) { + public async Task Handle( + MessageUpdate update, + CancellationToken cancellationToken + ) { // Handle slash commands if (update.Message.Entities?.FirstOrDefault() is { - Type: MessageEntityType.BotCommand, - Offset: 0 - }) { + Type: MessageEntityType.BotCommand, + Offset: 0 + }) { if (SlashCommand.TryCreate( - message: update.Message, - botUsername: (await _botProfileAccessor.GetBotProfileAsync(cancellationToken)).Username!, - commandPriorityCategorizer: _commandPriorityCategorizer, - out SlashCommand? slashCommand - )) { - await _commandQueue.DispatchAsync( + message: update.Message, + botUsername: (await botProfileAccessor.GetBotProfileAsync(cancellationToken)).Username!, + commandPriorityCategorizer: commandPriorityCategorizer, + out SlashCommand? slashCommand + )) { + await commandQueue.DispatchAsync( command: slashCommand ); } + return; } // Handle Social Link (better preview) if ((update.Message.Text ?? update.Message.Caption) is { } textOrCaption) { - IEnumerable possibleUrls = SocialLinkEmbedFixer.GetPossibleUrls(textOrCaption); + List possibleUrls = SocialLinkEmbedFixer.GetPossibleUrls(textOrCaption) + .ToList(); if (possibleUrls.Any()) { // Fire and forget - Task _ = Task.Run(async () => { - try { - foreach (Uri url in possibleUrls) { - Uri fixedUrl = SocialLinkEmbedFixer.Fix(url); - await _telegramBotClient.SendMessage( - chatId: update.Message.Chat.Id, - text: $"Preview: {fixedUrl.OriginalString}", - replyParameters: new ReplyParameters { - MessageId = update.Message.MessageId - }, - cancellationToken: cancellationToken - ); + Task _ = Task.Run( + async () => { + try { + foreach (Uri url in possibleUrls) { + Uri fixedUrl = SocialLinkEmbedFixer.Fix(url); + await telegramBotClient.SendMessage( + chatId: update.Message.Chat.Id, + text: $"Preview: {fixedUrl.OriginalString}", + replyParameters: new ReplyParameters { MessageId = update.Message.MessageId }, + cancellationToken: cancellationToken + ); + } + } catch (OperationCanceledException) { + // Terminate gracefully } - } catch (OperationCanceledException) { - // Terminate gracefully } - }); + ); return; } } // Handle reddit mirroring - if (update.Message?.Entities?.FirstOrDefault(entity => entity is { - Type: MessageEntityType.Url - }) is { - Offset: var offset, - Length: var length - } && update.Message.Text?.Substring(offset, length) is { } url - && url.StartsWith("https://www.reddit.com/", out string? remainingUrl)) { + if (update.Message.Entities?.FirstOrDefault( + entity => entity is { + Type: MessageEntityType.Url + } + ) is { + Offset: var offset, + Length: var length + } && + update.Message.Text?.Substring(offset, length) is { } url && + url.StartsWith("https://www.reddit.com/", out string? remainingUrl)) { // Fire and forget - Task _ = Task.Run(async () => { - try { - await _telegramBotClient.SendMessage( - chatId: update.Message.Chat.Id, - text: $"Mirror: https://libreddit.teknologiumum.com/{remainingUrl}", - replyParameters: new ReplyParameters { - MessageId = update.Message.MessageId - }, - linkPreviewOptions: new LinkPreviewOptions { - IsDisabled = true - }, - cancellationToken: cancellationToken - ); - } catch (OperationCanceledException) { - // Terminate gracefully + Task _ = Task.Run( + async () => { + try { + await telegramBotClient.SendMessage( + chatId: update.Message.Chat.Id, + text: $"Mirror: https://libreddit.teknologiumum.com/{remainingUrl}", + replyParameters: new ReplyParameters { MessageId = update.Message.MessageId }, + linkPreviewOptions: new LinkPreviewOptions { IsDisabled = true }, + cancellationToken: cancellationToken + ); + } catch (OperationCanceledException) { + // Terminate gracefully + } } - }); + ); return; - } else if (update.Message?.Entities?.FirstOrDefault(entity => entity is { - Type: MessageEntityType.TextLink - }) is { Url: { } textUrl } - && textUrl.StartsWith("https://www.reddit.com/", out string? remainingTextUrl)) { + } + + if (update.Message.Entities?.FirstOrDefault( + entity => entity is { + Type: MessageEntityType.TextLink + } + ) is { Url: { } textUrl } && + textUrl.StartsWith("https://www.reddit.com/", out string? remainingTextUrl)) { // Fire and forget - Task _ = Task.Run(async () => { - try { - await _telegramBotClient.SendMessage( - chatId: update.Message.Chat.Id, - text: $"Mirror: https://libreddit.teknologiumum.com/{remainingTextUrl}", - replyParameters: new ReplyParameters { - MessageId = update.Message.MessageId - }, - linkPreviewOptions: new LinkPreviewOptions { - IsDisabled = true - }, - cancellationToken: cancellationToken - ); - } catch (OperationCanceledException) { - // Terminate gracefully + Task _ = Task.Run( + async () => { + try { + await telegramBotClient.SendMessage( + chatId: update.Message.Chat.Id, + text: $"Mirror: https://libreddit.teknologiumum.com/{remainingTextUrl}", + replyParameters: new ReplyParameters { MessageId = update.Message.MessageId }, + linkPreviewOptions: new LinkPreviewOptions { IsDisabled = true }, + cancellationToken: cancellationToken + ); + } catch (OperationCanceledException) { + // Terminate gracefully + } } - }); + ); return; } // Handle AI calls - if (AICallCommand.TryCreate( - message: update.Message!, - commandPriorityCategorizer: _commandPriorityCategorizer, - out AICallCommand? aiCallCommand - )) { + if (AiCallCommand.TryCreate( + message: update.Message, + commandPriorityCategorizer: commandPriorityCategorizer, + out AiCallCommand? aiCallCommand + )) { // Cache both message and reply to message - _telegramMessageCache.Add( + telegramMessageCache.Add( message: aiCallCommand ); if (aiCallCommand.ReplyToMessage is { } replyToMessage) { - _telegramMessageCache.Add( + telegramMessageCache.Add( message: replyToMessage ); } - await _commandQueue.DispatchAsync( + await commandQueue.DispatchAsync( command: aiCallCommand ); return; @@ -147,96 +149,97 @@ await _commandQueue.DispatchAsync( // Handle AI follow up message if (update.Message is { - ReplyToMessage.MessageId: int replyToMessageId, - Chat.Id: long chatId - } && _telegramMessageCache.GetOrDefault( - messageId: new(replyToMessageId), - chatId: new(chatId) - ) is AIResponseMessage) { - if (!AIFollowUpMessage.TryCreate( - message: update.Message, - thread: _telegramMessageCache.GetThread( - messageId: new(replyToMessageId), - chatId: new(chatId) - ), - commandPriorityCategorizer: _commandPriorityCategorizer, - out AIFollowUpMessage? aiFollowUpMessage - )) { + ReplyToMessage.MessageId: int replyToMessageId, + Chat.Id: long chatId + } && + telegramMessageCache.GetOrDefault( + messageId: new(replyToMessageId), + chatId: new(chatId) + ) is AiResponseMessage) { + if (!AiFollowUpMessage.TryCreate( + message: update.Message, + thread: telegramMessageCache.GetThread( + messageId: new(replyToMessageId), + chatId: new(chatId) + ), + commandPriorityCategorizer: commandPriorityCategorizer, + out AiFollowUpMessage? aiFollowUpMessage + )) { return; } // Cache follow up message - _telegramMessageCache.Add( + telegramMessageCache.Add( message: aiFollowUpMessage ); - await _commandQueue.DispatchAsync(aiFollowUpMessage); + await commandQueue.DispatchAsync(aiFollowUpMessage); return; } - + // Handle SQL if (update.Message is { - ReplyToMessage: null, - Text: { } text - } && text.StartsWith("select", StringComparison.OrdinalIgnoreCase)) { + ReplyToMessage: null, + Text: { } text + } && + text.StartsWith("select", StringComparison.OrdinalIgnoreCase)) { try { - Sequence ast = new SqlParser.Parser().ParseSql(text); + Sequence ast = new Parser().ParseSql(text); if (ast.Count > 1) { // Fire and forget - Task _ = Task.Run(async () => { - try { - await _telegramBotClient.SendMessage( - chatId: update.Message.Chat.Id, - text: $"Your SQL contains more than one statement.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = update.Message.MessageId - }, - cancellationToken: cancellationToken - ); - } catch (OperationCanceledException) { - // Terminate gracefully + Task _ = Task.Run( + async () => { + try { + await telegramBotClient.SendMessage( + chatId: update.Message.Chat.Id, + text: $"Your SQL contains more than one statement.", + parseMode: ParseMode.Html, + replyParameters: new ReplyParameters { MessageId = update.Message.MessageId }, + cancellationToken: cancellationToken + ); + } catch (OperationCanceledException) { + // Terminate gracefully + } } - }); + ); return; } - if (ast[0] is not Statement.Select selectStatement) { + + if (ast[0] is not Statement.Select) { // Fire and forget - Task _ = Task.Run(async () => { - try { - await _telegramBotClient.SendMessage( - chatId: update.Message.Chat.Id, - text: $"Your SQL is not a SELECT statement.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = update.Message.MessageId - }, - cancellationToken: cancellationToken - ); - } catch (OperationCanceledException) { - // Terminate gracefully + Task _ = Task.Run( + async () => { + try { + await telegramBotClient.SendMessage( + chatId: update.Message.Chat.Id, + text: $"Your SQL is not a SELECT statement.", + parseMode: ParseMode.Html, + replyParameters: new ReplyParameters { MessageId = update.Message.MessageId }, + cancellationToken: cancellationToken + ); + } catch (OperationCanceledException) { + // Terminate gracefully + } } - }); + ); return; } - if (SQLCommand.TryCreate( - message: update.Message, - commandPriorityCategorizer: _commandPriorityCategorizer, - sqlCommand: out SQLCommand? sqlCommand - )) { - await _commandQueue.DispatchAsync( + + if (SqlCommand.TryCreate( + message: update.Message, + commandPriorityCategorizer: commandPriorityCategorizer, + sqlCommand: out SqlCommand? sqlCommand + )) { + await commandQueue.DispatchAsync( command: sqlCommand ); - return; } } catch (ParserException exc) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: update.Message.Chat.Id, text: $"{exc.Message}", parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = update.Message.MessageId - }, + replyParameters: new ReplyParameters { MessageId = update.Message.MessageId }, cancellationToken: cancellationToken ); } catch { diff --git a/BotNet.CommandHandlers/BotUpdate/Message/SlashCommandHandler.cs b/BotNet.CommandHandlers/BotUpdate/Message/SlashCommandHandler.cs index 342b53c..92cf33d 100644 --- a/BotNet.CommandHandlers/BotUpdate/Message/SlashCommandHandler.cs +++ b/BotNet.CommandHandlers/BotUpdate/Message/SlashCommandHandler.cs @@ -24,10 +24,6 @@ public sealed class SlashCommandHandler( ICommandQueue commandQueue, ITelegramMessageCache telegramMessageCache ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly ICommandQueue _commandQueue = commandQueue; - private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - public async Task Handle(SlashCommand command, CancellationToken cancellationToken) { try { switch (command.Command) { @@ -35,14 +31,14 @@ public async Task Handle(SlashCommand command, CancellationToken cancellationTok case "/flop": case "/flep": case "/flap": - await _commandQueue.DispatchAsync(FlipFlopCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(FlipFlopCommand.FromSlashCommand(command)); break; case "/evaljs": case "/evalcs": - await _commandQueue.DispatchAsync(EvalCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(EvalCommand.FromSlashCommand(command)); break; case "/fuck": - await _commandQueue.DispatchAsync(FuckCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(FuckCommand.FromSlashCommand(command)); break; case "/c": case "/clojure": @@ -69,51 +65,51 @@ public async Task Handle(SlashCommand command, CancellationToken cancellationTok case "/js": case "/ts": case "/vb": - await _commandQueue.DispatchAsync(ExecCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(ExecCommand.FromSlashCommand(command)); break; case "/pop": - await _commandQueue.DispatchAsync(PopCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(PopCommand.FromSlashCommand(command)); break; case "/ask": - await _commandQueue.DispatchAsync( + await commandQueue.DispatchAsync( command: AskCommand.FromSlashCommand( command: command, thread: command.ReplyToMessage is null - ? Enumerable.Empty() - : _telegramMessageCache.GetThread( + ? [] + : telegramMessageCache.GetThread( firstMessage: command.ReplyToMessage ) ) ); break; case "/humor": - await _commandQueue.DispatchAsync(HumorCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(HumorCommand.FromSlashCommand(command)); break; case "/primbon": - await _commandQueue.DispatchAsync(PrimbonCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(PrimbonCommand.FromSlashCommand(command)); break; case "/art": - await _commandQueue.DispatchAsync(ArtCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(ArtCommand.FromSlashCommand(command)); break; case "/bmkg": - await _commandQueue.DispatchAsync(BMKGCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(BmkgCommand.FromSlashCommand(command)); break; case "/map": - await _commandQueue.DispatchAsync(MapCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(MapCommand.FromSlashCommand(command)); break; case "/weather": - await _commandQueue.DispatchAsync(WeatherCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(WeatherCommand.FromSlashCommand(command)); break; case "/privilege": case "/start": - await _commandQueue.DispatchAsync(PrivilegeCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(PrivilegeCommand.FromSlashCommand(command)); break; case "/khodam": - await _commandQueue.DispatchAsync(KhodamCommand.FromSlashCommand(command)); + await commandQueue.DispatchAsync(KhodamCommand.FromSlashCommand(command)); break; } } catch (UsageException exc) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: exc.Message, parseMode: exc.ParseMode, diff --git a/BotNet.CommandHandlers/Eval/EvalCommandHandler.cs b/BotNet.CommandHandlers/Eval/EvalCommandHandler.cs index ce62072..201c3d9 100644 --- a/BotNet.CommandHandlers/Eval/EvalCommandHandler.cs +++ b/BotNet.CommandHandlers/Eval/EvalCommandHandler.cs @@ -14,23 +14,19 @@ public sealed class EvalCommandHandler( V8Evaluator v8Evaluator, CSharpEvaluator cSharpEvaluator ) : ICommandHandler { - private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { WriteIndented = true }; - - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly V8Evaluator _v8Evaluator = v8Evaluator; - private readonly CSharpEvaluator _cSharpEvaluator = cSharpEvaluator; + private static readonly JsonSerializerOptions JsonSerializerOptions = new() { WriteIndented = true }; public async Task Handle(EvalCommand command, CancellationToken cancellationToken) { string result; switch (command.Command) { case "/evaljs": try { - result = await _v8Evaluator.EvaluateAsync( + result = await v8Evaluator.EvaluateAsync( script: command.Code, cancellationToken: cancellationToken ); } catch (ScriptEngineException exc) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "" + WebUtility.HtmlEncode(exc.Message) + "", parseMode: ParseMode.Html, @@ -39,7 +35,7 @@ await _telegramBotClient.SendMessage( ); return; } catch (OperationCanceledException) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Timeout exceeded.", parseMode: ParseMode.Html, @@ -48,7 +44,7 @@ await _telegramBotClient.SendMessage( ); return; } catch (JsonException exc) when (exc.Message.Contains("A possible object cycle was detected.")) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "A possible object cycle was detected.", parseMode: ParseMode.Html, @@ -60,14 +56,14 @@ await _telegramBotClient.SendMessage( break; case "/evalcs": try { - object resultObject = _cSharpEvaluator.Evaluate( + object resultObject = cSharpEvaluator.Evaluate( expression: command.Code ); // Prettify result - result = JsonSerializer.Serialize(resultObject, JSON_SERIALIZER_OPTIONS); + result = JsonSerializer.Serialize(resultObject, JsonSerializerOptions); } catch (Exception exc) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "" + WebUtility.HtmlEncode(exc.Message) + "", parseMode: ParseMode.Html, @@ -82,7 +78,7 @@ await _telegramBotClient.SendMessage( } if (result.Length > 1000) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Result is too long.", parseMode: ParseMode.Html, @@ -90,7 +86,7 @@ await _telegramBotClient.SendMessage( cancellationToken: cancellationToken ); } else { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: result.Length >= 2 && result[0] == '"' && result[^1] == '"' ? $"Expression:\n{WebUtility.HtmlEncode(command.Code)}\n\nString Result:\n{WebUtility.HtmlEncode(result[1..^1].Replace("\\n", "\n"))}" diff --git a/BotNet.CommandHandlers/Exec/ExecCommandHandler.cs b/BotNet.CommandHandlers/Exec/ExecCommandHandler.cs index 5f4925e..59aaff0 100644 --- a/BotNet.CommandHandlers/Exec/ExecCommandHandler.cs +++ b/BotNet.CommandHandlers/Exec/ExecCommandHandler.cs @@ -15,10 +15,6 @@ public sealed class ExecCommandHandler( PistonClient pistonClient, ILogger logger ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly PistonClient _pistonClient = pistonClient; - private readonly ILogger _logger = logger; - public Task Handle(ExecCommand command, CancellationToken cancellationToken) { // Ignore non-mentioned commands in home group if (command.Chat is HomeGroupChat @@ -29,14 +25,14 @@ public Task Handle(ExecCommand command, CancellationToken cancellationToken) { // Fire and forget Task.Run(async () => { try { - ExecuteResult result = await _pistonClient.ExecuteAsync( + ExecuteResult result = await pistonClient.ExecuteAsync( language: command.PistonLanguageIdentifier, code: command.Code, cancellationToken: cancellationToken ); if (result.Compile is { Code: not 0 }) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $"{WebUtility.HtmlEncode(result.Compile.Stderr)}", parseMode: ParseMode.Html, @@ -44,7 +40,7 @@ await _telegramBotClient.SendMessage( cancellationToken: cancellationToken ); } else if (result.Run.Code != 0) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $"{WebUtility.HtmlEncode(result.Run.Stderr)}", parseMode: ParseMode.Html, @@ -52,7 +48,7 @@ await _telegramBotClient.SendMessage( cancellationToken: cancellationToken ); } else if (result.Run.Output.Length > 1000 || result.Run.Output.Count(c => c == '\n') > 20) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Output is too long.", parseMode: ParseMode.Html, @@ -60,7 +56,7 @@ await _telegramBotClient.SendMessage( cancellationToken: cancellationToken ); } else { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $"Code:\n```{command.HighlightLanguageIdentifier}\n{MarkdownV2Sanitizer.Sanitize(command.Code)}\n```\nOutput:\n```\n{MarkdownV2Sanitizer.Sanitize(result.Run.Output)}\n```", parseMode: ParseMode.MarkdownV2, @@ -71,15 +67,15 @@ await _telegramBotClient.SendMessage( #pragma warning disable CS0618 // Type or member is obsolete } catch (ExecutionEngineException exc) { #pragma warning restore CS0618 // Type or member is obsolete - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, - text: "" + WebUtility.HtmlEncode(exc.Message ?? "Unknown error") + "", + text: "" + WebUtility.HtmlEncode(exc.Message) + "", parseMode: ParseMode.Html, replyParameters: new ReplyParameters { MessageId = command.CodeMessageId }, cancellationToken: cancellationToken ); } catch (OperationCanceledException) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Timeout exceeded.", parseMode: ParseMode.Html, @@ -87,7 +83,7 @@ await _telegramBotClient.SendMessage( cancellationToken: cancellationToken ); } catch (Exception exc) { - _logger.LogError(exc, "Unhandled exception while executing code."); + logger.LogError(exc, "Unhandled exception while executing code."); } }); diff --git a/BotNet.CommandHandlers/FlipFlop/FlipFlopCommandHandler.cs b/BotNet.CommandHandlers/FlipFlop/FlipFlopCommandHandler.cs index 2929931..fac8416 100644 --- a/BotNet.CommandHandlers/FlipFlop/FlipFlopCommandHandler.cs +++ b/BotNet.CommandHandlers/FlipFlop/FlipFlopCommandHandler.cs @@ -8,12 +8,10 @@ namespace BotNet.CommandHandlers.FlipFlop { internal sealed class FlipFlopCommandHandler( ITelegramBotClient telegramBotClient ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - public async Task Handle(FlipFlopCommand command, CancellationToken cancellationToken) { // Download original image using MemoryStream originalImageStream = new(); - File fileInfo = await _telegramBotClient.GetInfoAndDownloadFile( + File fileInfo = await telegramBotClient.GetInfoAndDownloadFile( fileId: command.ImageFileId, destination: originalImageStream, cancellationToken: cancellationToken @@ -40,7 +38,7 @@ public async Task Handle(FlipFlopCommand command, CancellationToken cancellation // Send result image using MemoryStream resultImageStream = new(resultImage); - await _telegramBotClient.SendPhoto( + await telegramBotClient.SendPhoto( chatId: command.Chat.Id, photo: new InputFileStream(resultImageStream, new string(fileInfo.FileId.Reverse().ToArray()) + ".png"), replyParameters: new ReplyParameters { MessageId = command.ImageMessageId }, diff --git a/BotNet.CommandHandlers/Fuck/FuckCommandHandler.cs b/BotNet.CommandHandlers/Fuck/FuckCommandHandler.cs index 9074463..06d870c 100644 --- a/BotNet.CommandHandlers/Fuck/FuckCommandHandler.cs +++ b/BotNet.CommandHandlers/Fuck/FuckCommandHandler.cs @@ -9,14 +9,12 @@ namespace BotNet.CommandHandlers.Fuck { public sealed class FuckCommandHandler( ITelegramBotClient telegramBotClient ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - public async Task Handle(FuckCommand command, CancellationToken cancellationToken) { try { string stdout = BrainfuckInterpreter.RunBrainfuck( code: command.Code ); - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: WebUtility.HtmlEncode(stdout), parseMode: ParseMode.Html, @@ -24,7 +22,7 @@ await _telegramBotClient.SendMessage( cancellationToken: cancellationToken ); } catch (InvalidProgramException exc) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "" + WebUtility.HtmlEncode(exc.Message) + "", parseMode: ParseMode.Html, @@ -32,7 +30,7 @@ await _telegramBotClient.SendMessage( cancellationToken: cancellationToken ); } catch (IndexOutOfRangeException) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Memory access violation", parseMode: ParseMode.Html, @@ -40,7 +38,7 @@ await _telegramBotClient.SendMessage( cancellationToken: cancellationToken ); } catch (TimeoutException) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Operation timed out", parseMode: ParseMode.Html, diff --git a/BotNet.CommandHandlers/GoogleMaps/MapCommandHandler.cs b/BotNet.CommandHandlers/GoogleMaps/MapCommandHandler.cs index eab9b7f..032fb31 100644 --- a/BotNet.CommandHandlers/GoogleMaps/MapCommandHandler.cs +++ b/BotNet.CommandHandlers/GoogleMaps/MapCommandHandler.cs @@ -13,18 +13,13 @@ public sealed class MapCommandHandler( StaticMap staticMap, ILogger logger ) : ICommandHandler { - private static readonly RateLimiter SEARCH_PLACE_RATE_LIMITER = RateLimiter.PerUserPerChat(1, TimeSpan.FromMinutes(2)); - - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly GeoCode _geoCode = geoCode; - private readonly StaticMap _staticMap = staticMap; - private readonly ILogger _logger = logger; + private static readonly RateLimiter SearchPlaceRateLimiter = RateLimiter.PerUserPerChat(1, TimeSpan.FromMinutes(2)); public Task Handle(MapCommand command, CancellationToken cancellationToken) { try { - SEARCH_PLACE_RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); + SearchPlaceRateLimiter.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException exc) { - return _telegramBotClient.SendMessage( + return telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $"Anda belum mendapat giliran. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, @@ -36,10 +31,10 @@ public Task Handle(MapCommand command, CancellationToken cancellationToken) { // Fire and forget Task.Run(async () => { try { - (double lat, double lng) = await _geoCode.SearchPlaceAsync(command.PlaceName); - string staticMapUrl = _staticMap.SearchPlace(command.PlaceName); + (double lat, double lng) = await geoCode.SearchPlaceAsync(command.PlaceName); + string staticMapUrl = staticMap.SearchPlace(command.PlaceName); - await _telegramBotClient.SendPhoto( + await telegramBotClient.SendPhoto( chatId: command.Chat.Id, photo: new InputFileUrl(staticMapUrl), caption: $"View in 🗺️ Google Maps", @@ -50,7 +45,7 @@ await _telegramBotClient.SendPhoto( } catch (OperationCanceledException) { // Terminate gracefully } catch (Exception exc) { - _logger.LogError(exc, "Could not find place"); + logger.LogError(exc, "Could not find place"); await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Lokasi tidak dapat ditemukan", diff --git a/BotNet.CommandHandlers/Humor/HumorCommandHandler.cs b/BotNet.CommandHandlers/Humor/HumorCommandHandler.cs index a30da62..ebb624a 100644 --- a/BotNet.CommandHandlers/Humor/HumorCommandHandler.cs +++ b/BotNet.CommandHandlers/Humor/HumorCommandHandler.cs @@ -10,16 +10,13 @@ public sealed class HumorCommandHandler( ITelegramBotClient telegramBotClient, ProgrammerHumorScraper programmerHumorScraper ) : ICommandHandler { - private static readonly RateLimiter RATE_LIMITER = RateLimiter.PerChat(2, TimeSpan.FromMinutes(2)); - - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly ProgrammerHumorScraper _programmerHumorScraper = programmerHumorScraper; + private static readonly RateLimiter RateLimiter = RateLimiter.PerChat(2, TimeSpan.FromMinutes(2)); public Task Handle(HumorCommand command, CancellationToken cancellationToken) { try { - RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); + RateLimiter.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException exc) { - return _telegramBotClient.SendMessage( + return telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $"Bentar ya saya mikir dulu jokenya. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, @@ -31,10 +28,10 @@ public Task Handle(HumorCommand command, CancellationToken cancellationToken) { // Fire and forget Task.Run(async () => { try { - (string title, byte[] image) = await _programmerHumorScraper.GetRandomJokeAsync(cancellationToken); + (string title, byte[] image) = await programmerHumorScraper.GetRandomJokeAsync(cancellationToken); using MemoryStream imageStream = new(image); - await _telegramBotClient.SendPhoto( + await telegramBotClient.SendPhoto( chatId: command.Chat.Id, photo: new InputFileStream(imageStream, "joke.webp"), caption: title, diff --git a/BotNet.CommandHandlers/Khodam/KhodamCommandHandler.cs b/BotNet.CommandHandlers/Khodam/KhodamCommandHandler.cs index fd250cb..a049713 100644 --- a/BotNet.CommandHandlers/Khodam/KhodamCommandHandler.cs +++ b/BotNet.CommandHandlers/Khodam/KhodamCommandHandler.cs @@ -9,15 +9,13 @@ namespace BotNet.CommandHandlers.Khodam { public sealed class KhodamCommandHandler( ITelegramBotClient telegramBotClient ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - public async Task Handle(KhodamCommand command, CancellationToken cancellationToken) { string khodam = KhodamCalculator.CalculateKhodam( name: command.Name, userId: command.UserId ); - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $$""" Khodam {{WebUtility.HtmlEncode(command.Name)}} hari ini adalah... diff --git a/BotNet.CommandHandlers/Pop/BubbleWrapCallbackHandler.cs b/BotNet.CommandHandlers/Pop/BubbleWrapCallbackHandler.cs index 7fdfc51..a187436 100644 --- a/BotNet.CommandHandlers/Pop/BubbleWrapCallbackHandler.cs +++ b/BotNet.CommandHandlers/Pop/BubbleWrapCallbackHandler.cs @@ -8,11 +8,8 @@ public sealed class BubbleWrapCallbackHandler( ITelegramBotClient telegramBotClient, BubbleWrapKeyboardGenerator bubbleWrapKeyboardGenerator ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly BubbleWrapKeyboardGenerator _bubbleWrapKeyboardGenerator = bubbleWrapKeyboardGenerator; - public Task Handle(BubbleWrapCallback command, CancellationToken cancellationToken) { - InlineKeyboardMarkup poppedKeyboardMarkup = _bubbleWrapKeyboardGenerator.HandleCallback( + InlineKeyboardMarkup poppedKeyboardMarkup = bubbleWrapKeyboardGenerator.HandleCallback( chatId: command.ChatId, messageId: command.MessageId, sheetData: command.SheetData @@ -20,7 +17,7 @@ public Task Handle(BubbleWrapCallback command, CancellationToken cancellationTok // Fire and forget Task.Run(async () => { - await _telegramBotClient.EditMessageReplyMarkup( + await telegramBotClient.EditMessageReplyMarkup( chatId: command.ChatId, messageId: command.MessageId, replyMarkup: poppedKeyboardMarkup, diff --git a/BotNet.CommandHandlers/Pop/PopCommandHandler.cs b/BotNet.CommandHandlers/Pop/PopCommandHandler.cs index f235ce7..12adaaa 100644 --- a/BotNet.CommandHandlers/Pop/PopCommandHandler.cs +++ b/BotNet.CommandHandlers/Pop/PopCommandHandler.cs @@ -7,14 +7,12 @@ namespace BotNet.CommandHandlers.Pop { public sealed class PopCommandHandler( ITelegramBotClient telegramBotClient ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - public async Task Handle(PopCommand command, CancellationToken cancellationToken) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Here's a bubble wrap. Enjoy!", parseMode: ParseMode.Html, - replyMarkup: BubbleWrapKeyboardGenerator.EMPTY_KEYBOARD, + replyMarkup: BubbleWrapKeyboardGenerator.EmptyKeyboard, cancellationToken: cancellationToken ); } diff --git a/BotNet.CommandHandlers/Primbon/PrimbonCommandHandler.cs b/BotNet.CommandHandlers/Primbon/PrimbonCommandHandler.cs index 34e23a0..3b35e8b 100644 --- a/BotNet.CommandHandlers/Primbon/PrimbonCommandHandler.cs +++ b/BotNet.CommandHandlers/Primbon/PrimbonCommandHandler.cs @@ -12,20 +12,17 @@ public sealed class PrimbonCommandHandler( PrimbonScraper primbonScraper, ChineseCalendarScraper chineseCalendarScraper ) : ICommandHandler { - private static readonly RateLimiter RATE_LIMITER = RateLimiter.PerChat(2, TimeSpan.FromMinutes(2)); - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly PrimbonScraper _primbonScraper = primbonScraper; - private readonly ChineseCalendarScraper _chineseCalendarScraper = chineseCalendarScraper; + private static readonly RateLimiter RateLimiter = RateLimiter.PerChat(2, TimeSpan.FromMinutes(2)); public async Task Handle(PrimbonCommand command, CancellationToken cancellationToken) { try { - RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); + RateLimiter.ValidateActionRate(command.Chat.Id, command.Sender.Id); - (string javaneseDate, string sangar, string restriction) = await _primbonScraper.GetTaliwangkeAsync( + (string javaneseDate, string sangar, string restriction) = await primbonScraper.GetTaliwangkeAsync( date: command.Date, cancellationToken: cancellationToken ); - (string title, string[] traits) = await _primbonScraper.GetKamarokamAsync( + (string title, string[] traits) = await primbonScraper.GetKamarokamAsync( date: command.Date, cancellationToken: cancellationToken ); @@ -37,36 +34,36 @@ public async Task Handle(PrimbonCommand command, CancellationToken cancellationT string godOfWealth, string[] auspiciousActivities, string[] inauspiciousActivities - ) = await _chineseCalendarScraper.GetYellowCalendarAsync( + ) = await chineseCalendarScraper.GetYellowCalendarAsync( date: command.Date, cancellationToken: cancellationToken ); - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, - text: $$""" - {{javaneseDate}} + text: $""" + {javaneseDate} Petung Hari Baik - {{title}}: {{string.Join(", ", traits)}} + {title}: {string.Join(", ", traits)} Hari Larangan - {{sangar}}! {{restriction}} + {sangar}! {restriction} Chinese Calendar - Clash: {{clash}} Evil: {{evil}} - God of Joy: {{godOfJoy}} - God of Happiness: {{godOfHappiness}} - God of Wealth: {{godOfWealth}} - Auspicious Activities: {{string.Join(", ", auspiciousActivities)}} - Inauspicious Activities: {{string.Join(", ", inauspiciousActivities)}} + Clash: {clash} Evil: {evil} + God of Joy: {godOfJoy} + God of Happiness: {godOfHappiness} + God of Wealth: {godOfWealth} + Auspicious Activities: {string.Join(", ", auspiciousActivities)} + Inauspicious Activities: {string.Join(", ", inauspiciousActivities)} """, parseMode: ParseMode.Html, replyParameters: new ReplyParameters { MessageId = command.CommandMessageId }, cancellationToken: cancellationToken ); } catch (RateLimitExceededException exc) when (exc is { Cooldown: var cooldown }) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $"Coba lagi {cooldown}.", parseMode: ParseMode.Html, diff --git a/BotNet.CommandHandlers/Privilege/PrivilegeCommandHandler.cs b/BotNet.CommandHandlers/Privilege/PrivilegeCommandHandler.cs index 7588bdc..6af3355 100644 --- a/BotNet.CommandHandlers/Privilege/PrivilegeCommandHandler.cs +++ b/BotNet.CommandHandlers/Privilege/PrivilegeCommandHandler.cs @@ -1,5 +1,4 @@ using BotNet.Commands.ChatAggregate; -using BotNet.Commands.CommandPrioritization; using BotNet.Commands.Privilege; using BotNet.Commands.SenderAggregate; using BotNet.Services.RateLimit; @@ -9,17 +8,13 @@ namespace BotNet.CommandHandlers.Privilege { public sealed class PrivilegeCommandHandler( - ITelegramBotClient telegramBotClient, - CommandPriorityCategorizer commandPriorityCategorizer + ITelegramBotClient telegramBotClient ) : ICommandHandler { - private static readonly RateLimiter RATE_LIMITER = RateLimiter.PerChat(1, TimeSpan.FromMinutes(1)); - - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly CommandPriorityCategorizer _commandPriorityCategorizer = commandPriorityCategorizer; + private static readonly RateLimiter RateLimiter = RateLimiter.PerChat(1, TimeSpan.FromMinutes(1)); public Task Handle(PrivilegeCommand command, CancellationToken cancellationToken) { try { - RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); + RateLimiter.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException) { // Silently reject commands after rate limit exceeded return Task.CompletedTask; @@ -29,8 +24,8 @@ public Task Handle(PrivilegeCommand command, CancellationToken cancellationToken Task.Run(async () => { try { switch (command) { - case { Chat: PrivateChat, Sender: VIPSender }: - await _telegramBotClient.SendMessage( + case { Chat: PrivateChat, Sender: VipSender }: + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $$""" 👑 Anda adalah user VIP (ID: {{command.Sender.Id}}) @@ -45,7 +40,7 @@ await _telegramBotClient.SendMessage( ); break; case { Chat: PrivateChat }: - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $$""" ❌ Feature bot dibatasi di dalam private chat (ID: {{command.Sender.Id}}) @@ -59,8 +54,8 @@ await _telegramBotClient.SendMessage( cancellationToken: cancellationToken ); break; - case { Chat: HomeGroupChat, Sender: VIPSender }: - await _telegramBotClient.SendMessage( + case { Chat: HomeGroupChat, Sender: VipSender }: + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $$""" 👑 Group {{command.Chat.Title}} (ID: {{command.Chat.Id}}) adalah home group @@ -78,7 +73,7 @@ await _telegramBotClient.SendMessage( ); break; case { Chat: HomeGroupChat }: - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $$""" 👑 Group {{command.Chat.Title}} (ID: {{command.Chat.Id}}) adalah home group @@ -92,8 +87,8 @@ await _telegramBotClient.SendMessage( cancellationToken: cancellationToken ); break; - case { Chat: GroupChat, Sender: VIPSender }: - await _telegramBotClient.SendMessage( + case { Chat: GroupChat, Sender: VipSender }: + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $$""" ⚠️ Bot dipakai di group selain home group (ID: {{command.Chat.Id}}) @@ -113,7 +108,7 @@ await _telegramBotClient.SendMessage( ); break; case { Chat: GroupChat }: - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $$""" ⚠️ Bot dipakai di group selain home group (ID: {{command.Chat.Id}}) diff --git a/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs b/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs index 2edb3dc..74a237c 100644 --- a/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs +++ b/BotNet.CommandHandlers/SQL/SQLCommandHandler.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Globalization; +using System.Text; using BotNet.Commands.SQL; using BotNet.Services.SQL; using BotNet.Services.Sqlite; @@ -10,21 +11,18 @@ using Telegram.Bot.Types.Enums; namespace BotNet.CommandHandlers.SQL { - public sealed class SQLCommandHandler( + public sealed class SqlCommandHandler( ITelegramBotClient telegramBotClient, IServiceProvider serviceProvider - ) : ICommandHandler { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly IServiceProvider _serviceProvider = serviceProvider; - - public async Task Handle(SQLCommand command, CancellationToken cancellationToken) { + ) : ICommandHandler { + public async Task Handle(SqlCommand command, CancellationToken cancellationToken) { if (command.SelectStatement.Query.Body.AsSelectExpression().Select.From is not { } froms || froms.Count == 0) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "No FROM clause found.", parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { MessageId = command.SQLMessageId }, + replyParameters: new ReplyParameters { MessageId = command.SqlMessageId }, cancellationToken: cancellationToken ); return; @@ -47,16 +45,16 @@ await _telegramBotClient.SendMessage( } // Create scoped for scoped database - using IServiceScope serviceScope = _serviceProvider.CreateScope(); + using IServiceScope serviceScope = serviceProvider.CreateScope(); // Load tables into memory foreach (string table in tables) { IScopedDataSource? dataSource = serviceScope.ServiceProvider.GetKeyedService(table); if (dataSource == null) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, - text: $$""" - Table '{{table}}' not found. Available tables are: + text: $""" + Table '{table}' not found. Available tables are: - pileg_dpr_dapil - pileg_dpr_<kodedapil> - pileg_dpr_provinsi @@ -65,7 +63,7 @@ await _telegramBotClient.SendMessage( """, parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { MessageId = command.SQLMessageId }, + replyParameters: new ReplyParameters { MessageId = command.SqlMessageId }, cancellationToken: cancellationToken ); return; @@ -101,25 +99,25 @@ await _telegramBotClient.SendMessage( Type fieldType = reader.GetFieldType(i); if (fieldType == typeof(string)) { - values[i] = '"' + reader.GetString(i).Replace("\"", "\"\"") + '"'; + values[i] = $"\"{reader.GetString(i).Replace("\"", "\"\"")}\""; } else if (fieldType == typeof(int)) { values[i] = reader.GetInt32(i).ToString(); } else if (fieldType == typeof(long)) { values[i] = reader.GetInt64(i).ToString(); } else if (fieldType == typeof(float)) { - values[i] = reader.GetFloat(i).ToString(); + values[i] = reader.GetFloat(i).ToString(CultureInfo.InvariantCulture); } else if (fieldType == typeof(double)) { - values[i] = reader.GetDouble(i).ToString(); + values[i] = reader.GetDouble(i).ToString(CultureInfo.InvariantCulture); } else if (fieldType == typeof(decimal)) { - values[i] = reader.GetDecimal(i).ToString(); + values[i] = reader.GetDecimal(i).ToString(CultureInfo.InvariantCulture); } else if (fieldType == typeof(bool)) { values[i] = reader.GetBoolean(i).ToString(); } else if (fieldType == typeof(DateTime)) { - values[i] = reader.GetDateTime(i).ToString(); + values[i] = reader.GetDateTime(i).ToString(CultureInfo.InvariantCulture); } else if (fieldType == typeof(byte[])) { - values[i] = BitConverter.ToString(reader.GetFieldValue(i)).Replace("-", ""); + values[i] = Convert.ToHexString(reader.GetFieldValue(i)); } else { - values[i] = reader[i]?.ToString() ?? ""; + values[i] = reader[i].ToString() ?? ""; } } resultBuilder.AppendLine(string.Join(',', values)); @@ -128,11 +126,11 @@ await _telegramBotClient.SendMessage( } ); } catch (SqliteException exc) { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "" + exc.Message.Replace("SQLite Error", "Error") + "", parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { MessageId = command.SQLMessageId }, + replyParameters: new ReplyParameters { MessageId = command.SqlMessageId }, cancellationToken: cancellationToken ); return; @@ -141,19 +139,19 @@ await _telegramBotClient.SendMessage( // Send result string csvResult = resultBuilder.ToString(); if (csvResult.Length > 4000) { - await _telegramBotClient.SendDocument( + await telegramBotClient.SendDocument( chatId: command.Chat.Id, caption: $"{rows} rows affected", document: new InputFileStream(new MemoryStream(Encoding.UTF8.GetBytes(csvResult)), "result.csv"), - replyParameters: new ReplyParameters { MessageId = command.SQLMessageId }, + replyParameters: new ReplyParameters { MessageId = command.SqlMessageId }, cancellationToken: cancellationToken ); } else { - await _telegramBotClient.SendMessage( + await telegramBotClient.SendMessage( chatId: command.Chat.Id, - text: "```csv\n" + resultBuilder.ToString() + $"```\n{rows} rows affected", + text: $"```csv\n{resultBuilder}```\n{rows} rows affected", parseMode: ParseMode.MarkdownV2, - replyParameters: new ReplyParameters { MessageId = command.SQLMessageId }, + replyParameters: new ReplyParameters { MessageId = command.SqlMessageId }, cancellationToken: cancellationToken ); } @@ -178,9 +176,9 @@ private static void CollectTableNames(ref HashSet tables, TableFactor ta } } break; - case TableFactor.Function function: + case TableFactor.Function: break; - case TableFactor.JsonTable jsonTable: + case TableFactor.JsonTable: break; case TableFactor.NestedJoin nestedJoin: if (nestedJoin.TableWithJoins != null) { @@ -203,9 +201,9 @@ private static void CollectTableNames(ref HashSet tables, TableFactor ta case TableFactor.Table table: tables.Add(table.Name.ToString()); break; - case TableFactor.TableFunction tableFunction: + case TableFactor.TableFunction: break; - case TableFactor.UnNest unNest: + case TableFactor.UnNest: break; case TableFactor.Unpivot unpivot: tables.Add(unpivot.Name.ToString()); diff --git a/BotNet.CommandHandlers/TelegramMessageCache.cs b/BotNet.CommandHandlers/TelegramMessageCache.cs index 8eecf39..acbec33 100644 --- a/BotNet.CommandHandlers/TelegramMessageCache.cs +++ b/BotNet.CommandHandlers/TelegramMessageCache.cs @@ -7,19 +7,18 @@ namespace BotNet.CommandHandlers { internal sealed class TelegramMessageCache( IMemoryCache memoryCache ) : ITelegramMessageCache { - private static readonly TimeSpan CACHE_TTL = TimeSpan.FromHours(1); - private readonly IMemoryCache _memoryCache = memoryCache; + private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(1); public void Add(MessageBase message) { - _memoryCache.Set( + memoryCache.Set( key: new Key(message.MessageId, message.Chat.Id), value: message, - absoluteExpirationRelativeToNow: CACHE_TTL + absoluteExpirationRelativeToNow: CacheTtl ); } public MessageBase? GetOrDefault(MessageId messageId, ChatId chatId) { - if (_memoryCache.TryGetValue( + if (memoryCache.TryGetValue( key: new Key(messageId, chatId), value: out MessageBase? message )) { @@ -42,10 +41,12 @@ public IEnumerable GetThread(MessageId messageId, ChatId chatId) { public IEnumerable GetThread(MessageBase firstMessage) { yield return firstMessage; Add(firstMessage); - if (firstMessage.ReplyToMessage is not null) { - foreach (MessageBase reply in GetThread(firstMessage.ReplyToMessage.MessageId, firstMessage.Chat.Id)) { - yield return reply; - } + if (firstMessage.ReplyToMessage is null) { + yield break; + } + + foreach (MessageBase reply in GetThread(firstMessage.ReplyToMessage.MessageId, firstMessage.Chat.Id)) { + yield return reply; } } diff --git a/BotNet.CommandHandlers/Weather/WeatherCommandHandler.cs b/BotNet.CommandHandlers/Weather/WeatherCommandHandler.cs index 776547e..d886e85 100644 --- a/BotNet.CommandHandlers/Weather/WeatherCommandHandler.cs +++ b/BotNet.CommandHandlers/Weather/WeatherCommandHandler.cs @@ -12,17 +12,13 @@ public sealed class WeatherCommandHandler( CurrentWeather currentWeather, ILogger logger ) : ICommandHandler { - private static readonly RateLimiter GET_WEATHER_RATE_LIMITER = RateLimiter.PerUserPerChat(3, TimeSpan.FromMinutes(2)); - - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly CurrentWeather _currentWeather = currentWeather; - private readonly ILogger _logger = logger; + private static readonly RateLimiter GetWeatherRateLimiter = RateLimiter.PerUserPerChat(3, TimeSpan.FromMinutes(2)); public Task Handle(WeatherCommand command, CancellationToken cancellationToken) { try { - GET_WEATHER_RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); + GetWeatherRateLimiter.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException exc) { - return _telegramBotClient.SendMessage( + return telegramBotClient.SendMessage( chatId: command.Chat.Id, text: $"Anda belum mendapat giliran. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, @@ -34,12 +30,12 @@ public Task Handle(WeatherCommand command, CancellationToken cancellationToken) // Fire and forget Task.Run(async () => { try { - (string title, string icon) = await _currentWeather.GetCurrentWeatherAsync( + (string title, string icon) = await currentWeather.GetCurrentWeatherAsync( place: command.CityName, cancellationToken: cancellationToken ); - await _telegramBotClient.SendPhoto( + await telegramBotClient.SendPhoto( chatId: command.Chat.Id, photo: new InputFileUrl(icon), caption: title, @@ -50,8 +46,8 @@ await _telegramBotClient.SendPhoto( } catch (OperationCanceledException) { // Terminate gracefully } catch (Exception exc) { - _logger.LogError(exc, "Could not get weather"); - await _telegramBotClient.SendMessage( + logger.LogError(exc, "Could not get weather"); + await telegramBotClient.SendMessage( chatId: command.Chat.Id, text: "Lokasi tidak dapat ditemukan", parseMode: ParseMode.Html, diff --git a/BotNet.Commands/AI/Gemini/GeminiTextPrompt.cs b/BotNet.Commands/AI/Gemini/GeminiTextPrompt.cs index 0d5b8f8..a3c30b5 100644 --- a/BotNet.Commands/AI/Gemini/GeminiTextPrompt.cs +++ b/BotNet.Commands/AI/Gemini/GeminiTextPrompt.cs @@ -16,7 +16,7 @@ IEnumerable thread Thread = thread; } - public static GeminiTextPrompt FromAICallCommand(AICallCommand aiCallCommand, IEnumerable thread) { + public static GeminiTextPrompt FromAiCallCommand(AiCallCommand aiCallCommand, IEnumerable thread) { // Call sign must be Gemini, AI, or Bot if (aiCallCommand.CallSign is not "Gemini" and not "AI" and not "Bot") { throw new ArgumentException("Call sign must be Gemini, AI, or Bot", nameof(aiCallCommand)); @@ -28,24 +28,31 @@ public static GeminiTextPrompt FromAICallCommand(AICallCommand aiCallCommand, IE } // Non-empty thread must begin with reply to message - if (thread.FirstOrDefault() is { - MessageId: { } firstMessageId, - Chat.Id: { } firstChatId - }) { - if (firstMessageId != aiCallCommand.ReplyToMessage?.MessageId - || firstChatId != aiCallCommand.Chat.Id) { - throw new ArgumentException("Thread must begin with reply to message", nameof(thread)); - } + IEnumerable messageBases = thread as MessageBase[] ?? thread.ToArray(); + if (messageBases.FirstOrDefault() is not { + MessageId: var firstMessageId, + Chat.Id: var firstChatId + }) { + return new GeminiTextPrompt( + prompt: aiCallCommand.Text, + command: aiCallCommand, + thread: messageBases + ); } - return new( + if (firstMessageId != aiCallCommand.ReplyToMessage?.MessageId + || firstChatId != aiCallCommand.Chat.Id) { + throw new ArgumentException("Thread must begin with reply to message", nameof(thread)); + } + + return new GeminiTextPrompt( prompt: aiCallCommand.Text, command: aiCallCommand, - thread: thread + thread: messageBases ); } - public static GeminiTextPrompt FromAIFollowUpMessage(AIFollowUpMessage aIFollowUpMessage, IEnumerable thread) { + public static GeminiTextPrompt FromAiFollowUpMessage(AiFollowUpMessage aIFollowUpMessage, IEnumerable thread) { // Call sign must be Gemini, AI, or Bot if (aIFollowUpMessage.CallSign is not "Gemini" and not "AI" and not "Bot") { throw new ArgumentException("Call sign must be Gemini, AI, or Bot", nameof(aIFollowUpMessage)); @@ -57,20 +64,27 @@ public static GeminiTextPrompt FromAIFollowUpMessage(AIFollowUpMessage aIFollowU } // Non-empty thread must begin with reply to message - if (thread.FirstOrDefault() is { - MessageId: { } firstMessageId, - Chat.Id: { } firstChatId - }) { - if (firstMessageId != aIFollowUpMessage.ReplyToMessage?.MessageId - || firstChatId != aIFollowUpMessage.Chat.Id) { - throw new ArgumentException("Thread must begin with reply to message", nameof(thread)); - } + IEnumerable messageBases = thread as MessageBase[] ?? thread.ToArray(); + if (messageBases.FirstOrDefault() is not { + MessageId: var firstMessageId, + Chat.Id: var firstChatId + }) { + return new GeminiTextPrompt( + prompt: aIFollowUpMessage.Text, + command: aIFollowUpMessage, + thread: messageBases + ); + } + + if (firstMessageId != aIFollowUpMessage.ReplyToMessage.MessageId + || firstChatId != aIFollowUpMessage.Chat.Id) { + throw new ArgumentException("Thread must begin with reply to message", nameof(thread)); } - return new( + return new GeminiTextPrompt( prompt: aIFollowUpMessage.Text, command: aIFollowUpMessage, - thread: thread + thread: messageBases ); } } diff --git a/BotNet.Commands/AI/OpenAI/OpenAIImageGenerationPrompt.cs b/BotNet.Commands/AI/OpenAI/OpenAIImageGenerationPrompt.cs index 01320d3..7184839 100644 --- a/BotNet.Commands/AI/OpenAI/OpenAIImageGenerationPrompt.cs +++ b/BotNet.Commands/AI/OpenAI/OpenAIImageGenerationPrompt.cs @@ -3,7 +3,7 @@ using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.AI.OpenAI { - public sealed record class OpenAIImageGenerationPrompt : ICommand { + public sealed record OpenAiImageGenerationPrompt : ICommand { public string CallSign { get; } public string Prompt { get; } public MessageId PromptMessageId { get; } @@ -11,7 +11,7 @@ public sealed record class OpenAIImageGenerationPrompt : ICommand { public ChatBase Chat { get; } public HumanSender Sender { get; } - public OpenAIImageGenerationPrompt( + public OpenAiImageGenerationPrompt( string callSign, string prompt, MessageId promptMessageId, diff --git a/BotNet.Commands/AI/OpenAI/OpenAIImagePrompt.cs b/BotNet.Commands/AI/OpenAI/OpenAIImagePrompt.cs index 4f8657f..bd279aa 100644 --- a/BotNet.Commands/AI/OpenAI/OpenAIImagePrompt.cs +++ b/BotNet.Commands/AI/OpenAI/OpenAIImagePrompt.cs @@ -1,14 +1,14 @@ using BotNet.Commands.BotUpdate.Message; namespace BotNet.Commands.AI.OpenAI { - public sealed record OpenAIImagePrompt : ICommand { + public sealed record OpenAiImagePrompt : ICommand { public string CallSign { get; } public string Prompt { get; } public string ImageFileId { get; } public HumanMessageBase Command { get; } public IEnumerable Thread { get; } - private OpenAIImagePrompt( + private OpenAiImagePrompt( string callSign, string prompt, string imageFileId, @@ -22,7 +22,7 @@ IEnumerable thread Thread = thread; } - public static OpenAIImagePrompt FromAICallCommand(AICallCommand aiCallCommand, IEnumerable thread) { + public static OpenAiImagePrompt FromAiCallCommand(AiCallCommand aiCallCommand, IEnumerable thread) { // Call sign must be GPT if (aiCallCommand.CallSign is not "GPT") { throw new ArgumentException("Call sign must be GPT.", nameof(aiCallCommand)); @@ -35,31 +35,40 @@ public static OpenAIImagePrompt FromAICallCommand(AICallCommand aiCallCommand, I // File ID must be non-empty string imageFileId; + IEnumerable messageBases = thread as MessageBase[] ?? thread.ToArray(); if (!string.IsNullOrWhiteSpace(aiCallCommand.ImageFileId)) { imageFileId = aiCallCommand.ImageFileId; - } else if (!string.IsNullOrWhiteSpace(thread.FirstOrDefault()?.ImageFileId)) { - imageFileId = thread.First().ImageFileId!; + } else if (!string.IsNullOrWhiteSpace(messageBases.FirstOrDefault()?.ImageFileId)) { + imageFileId = messageBases.First().ImageFileId!; } else { throw new ArgumentException("File ID must be non-empty.", nameof(aiCallCommand)); } // Non-empty thread must begin with reply to message - if (thread.FirstOrDefault() is { - MessageId: { } firstMessageId, - Chat.Id: { } firstChatId - }) { - if (firstMessageId != aiCallCommand.ReplyToMessage?.MessageId - || firstChatId != aiCallCommand.Chat.Id) { - throw new ArgumentException("Thread must begin with reply to message.", nameof(thread)); - } + if (messageBases.FirstOrDefault() is not { + MessageId: var firstMessageId, + Chat.Id: var firstChatId + }) { + return new( + callSign: aiCallCommand.CallSign, + prompt: aiCallCommand.Text, + imageFileId: imageFileId, + command: aiCallCommand, + thread: messageBases + ); } - return new( + if (firstMessageId != aiCallCommand.ReplyToMessage?.MessageId + || firstChatId != aiCallCommand.Chat.Id) { + throw new ArgumentException("Thread must begin with reply to message.", nameof(thread)); + } + + return new OpenAiImagePrompt( callSign: aiCallCommand.CallSign, prompt: aiCallCommand.Text, imageFileId: imageFileId, command: aiCallCommand, - thread: thread + thread: messageBases ); } } diff --git a/BotNet.Commands/AI/OpenAI/OpenAITextPrompt.cs b/BotNet.Commands/AI/OpenAI/OpenAITextPrompt.cs index 2f6f706..be744f9 100644 --- a/BotNet.Commands/AI/OpenAI/OpenAITextPrompt.cs +++ b/BotNet.Commands/AI/OpenAI/OpenAITextPrompt.cs @@ -1,13 +1,13 @@ using BotNet.Commands.BotUpdate.Message; namespace BotNet.Commands.AI.OpenAI { - public sealed record OpenAITextPrompt : ICommand { + public sealed record OpenAiTextPrompt : ICommand { public string CallSign { get; } public string Prompt { get; } public HumanMessageBase Command { get; } public IEnumerable Thread { get; } - private OpenAITextPrompt( + private OpenAiTextPrompt( string callSign, string prompt, HumanMessageBase command, @@ -19,7 +19,7 @@ IEnumerable thread Thread = thread; } - public static OpenAITextPrompt FromAICallCommand(AICallCommand aiCallCommand, IEnumerable thread) { + public static OpenAiTextPrompt FromAiCallCommand(AiCallCommand aiCallCommand, IEnumerable thread) { // Call sign must be GPT if (aiCallCommand.CallSign is not "GPT") { throw new ArgumentException("Call sign must be GPT.", nameof(aiCallCommand)); @@ -31,25 +31,33 @@ public static OpenAITextPrompt FromAICallCommand(AICallCommand aiCallCommand, IE } // Non-empty thread must begin with reply to message - if (thread.FirstOrDefault() is { - MessageId: { } firstMessageId, - Chat.Id: { } firstChatId - }) { - if (firstMessageId != aiCallCommand.ReplyToMessage?.MessageId - || firstChatId != aiCallCommand.Chat.Id) { - throw new ArgumentException("Thread must begin with reply to message.", nameof(thread)); - } + IEnumerable messageBases = thread as MessageBase[] ?? thread.ToArray(); + if (messageBases.FirstOrDefault() is not { + MessageId: var firstMessageId, + Chat.Id: var firstChatId + }) { + return new( + callSign: aiCallCommand.CallSign, + prompt: aiCallCommand.Text, + command: aiCallCommand, + thread: messageBases + ); + } + + if (firstMessageId != aiCallCommand.ReplyToMessage?.MessageId + || firstChatId != aiCallCommand.Chat.Id) { + throw new ArgumentException("Thread must begin with reply to message.", nameof(thread)); } return new( callSign: aiCallCommand.CallSign, prompt: aiCallCommand.Text, command: aiCallCommand, - thread: thread + thread: messageBases ); } - public static OpenAITextPrompt FromAIFollowUpMessage(AIFollowUpMessage aiFollowUpMessage, IEnumerable thread) { + public static OpenAiTextPrompt FromAiFollowUpMessage(AiFollowUpMessage aiFollowUpMessage, IEnumerable thread) { // Call sign must be GPT if (aiFollowUpMessage.CallSign is not "GPT") { throw new ArgumentException("Call sign must be GPT.", nameof(aiFollowUpMessage)); @@ -61,21 +69,29 @@ public static OpenAITextPrompt FromAIFollowUpMessage(AIFollowUpMessage aiFollowU } // Non-empty thread must begin with reply to message - if (thread.FirstOrDefault() is { - MessageId: { } firstMessageId, - Chat.Id: { } firstChatId - }) { - if (firstMessageId != aiFollowUpMessage.ReplyToMessage.MessageId - || firstChatId != aiFollowUpMessage.Chat.Id) { - throw new ArgumentException("Thread must begin with reply to message.", nameof(thread)); - } + IEnumerable messageBases = thread as MessageBase[] ?? thread.ToArray(); + if (messageBases.FirstOrDefault() is not { + MessageId: var firstMessageId, + Chat.Id: var firstChatId + }) { + return new( + callSign: aiFollowUpMessage.CallSign, + prompt: aiFollowUpMessage.Text, + command: aiFollowUpMessage, + thread: messageBases + ); + } + + if (firstMessageId != aiFollowUpMessage.ReplyToMessage.MessageId + || firstChatId != aiFollowUpMessage.Chat.Id) { + throw new ArgumentException("Thread must begin with reply to message.", nameof(thread)); } return new( callSign: aiFollowUpMessage.CallSign, prompt: aiFollowUpMessage.Text, command: aiFollowUpMessage, - thread: thread + thread: messageBases ); } } diff --git a/BotNet.Commands/BMKG/BMKGCommand.cs b/BotNet.Commands/BMKG/BMKGCommand.cs index 5ea9b03..54d0bb3 100644 --- a/BotNet.Commands/BMKG/BMKGCommand.cs +++ b/BotNet.Commands/BMKG/BMKGCommand.cs @@ -3,12 +3,12 @@ using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.BMKG { - public sealed record BMKGCommand : ICommand { + public sealed record BmkgCommand : ICommand { public MessageId CommandMessageId { get; } public ChatBase Chat { get; } public HumanSender Sender { get; } - private BMKGCommand( + private BmkgCommand( MessageId commandMessageId, ChatBase chat, HumanSender sender @@ -18,7 +18,7 @@ HumanSender sender Sender = sender; } - public static BMKGCommand FromSlashCommand(SlashCommand slashCommand) { + public static BmkgCommand FromSlashCommand(SlashCommand slashCommand) { // Must be /bmkg if (slashCommand.Command != "/bmkg") { throw new ArgumentException("Command must be /bmkg.", nameof(slashCommand)); diff --git a/BotNet.Commands/BotUpdate/Message/AICallCommand.cs b/BotNet.Commands/BotUpdate/Message/AICallCommand.cs index 0f97d58..c46d0b6 100644 --- a/BotNet.Commands/BotUpdate/Message/AICallCommand.cs +++ b/BotNet.Commands/BotUpdate/Message/AICallCommand.cs @@ -5,8 +5,8 @@ using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.BotUpdate.Message { - public sealed record AICallCommand : HumanMessageBase, ICommand { - public static readonly ImmutableHashSet CALL_SIGNS = [ + public sealed record AiCallCommand : HumanMessageBase, ICommand { + private static readonly ImmutableHashSet CALL_SIGNS = [ "AI", "Bot", "GPT", @@ -16,7 +16,7 @@ public sealed record AICallCommand : HumanMessageBase, ICommand { public string CallSign { get; } - private AICallCommand( + private AiCallCommand( MessageId messageId, ChatBase chat, HumanSender sender, @@ -38,7 +38,7 @@ string callSign public static bool TryCreate( Telegram.Bot.Types.Message message, CommandPriorityCategorizer commandPriorityCategorizer, - [NotNullWhen(true)] out AICallCommand? aiCallCommand + [NotNullWhen(true)] out AiCallCommand? aiCallCommand ) { // Chat must be private or group if (!ChatBase.TryCreate(message.Chat, commandPriorityCategorizer, out ChatBase? chat)) { diff --git a/BotNet.Commands/BotUpdate/Message/AIFollowUpMessage.cs b/BotNet.Commands/BotUpdate/Message/AIFollowUpMessage.cs index 01765e0..ca1422f 100644 --- a/BotNet.Commands/BotUpdate/Message/AIFollowUpMessage.cs +++ b/BotNet.Commands/BotUpdate/Message/AIFollowUpMessage.cs @@ -4,17 +4,17 @@ using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.BotUpdate.Message { - public sealed record AIFollowUpMessage : HumanMessageBase, ICommand { - public override AIResponseMessage ReplyToMessage => (AIResponseMessage)base.ReplyToMessage!; + public sealed record AiFollowUpMessage : HumanMessageBase, ICommand { + public override AiResponseMessage ReplyToMessage => (AiResponseMessage)base.ReplyToMessage!; public string CallSign => ReplyToMessage.CallSign; - public AIFollowUpMessage( + private AiFollowUpMessage( MessageId messageId, ChatBase chat, HumanSender sender, string text, string? imageFileId, - AIResponseMessage replyToMessage + AiResponseMessage replyToMessage ) : base( messageId: messageId, chat: chat, @@ -28,7 +28,7 @@ public static bool TryCreate( Telegram.Bot.Types.Message message, IEnumerable thread, CommandPriorityCategorizer commandPriorityCategorizer, - [NotNullWhen(true)] out AIFollowUpMessage? aiFollowUpMessage + [NotNullWhen(true)] out AiFollowUpMessage? aiFollowUpMessage ) { // Chat must be private or group if (!ChatBase.TryCreate(message.Chat, commandPriorityCategorizer, out ChatBase? chat)) { @@ -37,8 +37,8 @@ public static bool TryCreate( } // Sender must be a user - if (message.From is not { } from - || !HumanSender.TryCreate(from, commandPriorityCategorizer, out HumanSender? sender)) { + if (message.From is not { } from || + !HumanSender.TryCreate(from, commandPriorityCategorizer, out HumanSender? sender)) { aiFollowUpMessage = null; return false; } @@ -50,33 +50,27 @@ public static bool TryCreate( } // Must reply to AI response message - if (thread.FirstOrDefault() is not AIResponseMessage { CallSign: string callSign } aiResponseMessage - || aiResponseMessage.MessageId != message.ReplyToMessage?.MessageId) { + if (thread.FirstOrDefault() is not AiResponseMessage { CallSign: string } aiResponseMessage || + aiResponseMessage.MessageId != message.ReplyToMessage?.MessageId) { aiFollowUpMessage = null; return false; } // Sender must be a user if (message.From is not { - IsBot: false, - Id: long senderId, - FirstName: string senderFirstName, - LastName: var senderLastName - }) { + IsBot: false, + }) { aiFollowUpMessage = null; return false; } - string senderFullName = senderLastName is null - ? senderFirstName - : $"{senderFirstName} {senderLastName}"; - aiFollowUpMessage = new( messageId: new(message.MessageId), chat: chat, sender: sender, text: text, - imageFileId: message.Photo?.FirstOrDefault()?.FileId, + imageFileId: message.Photo?.FirstOrDefault() + ?.FileId, replyToMessage: aiResponseMessage ); return true; diff --git a/BotNet.Commands/BotUpdate/Message/AIResponseMessage.cs b/BotNet.Commands/BotUpdate/Message/AIResponseMessage.cs index 5c8bd8b..32a010a 100644 --- a/BotNet.Commands/BotUpdate/Message/AIResponseMessage.cs +++ b/BotNet.Commands/BotUpdate/Message/AIResponseMessage.cs @@ -3,10 +3,10 @@ using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.BotUpdate.Message { - public sealed record AIResponseMessage : MessageBase { + public sealed record AiResponseMessage : MessageBase { public string CallSign { get; } - private AIResponseMessage( + private AiResponseMessage( MessageId messageId, ChatBase chat, BotSender sender, @@ -25,7 +25,7 @@ string callSign CallSign = callSign; } - public static AIResponseMessage FromMessage( + public static AiResponseMessage FromMessage( Telegram.Bot.Types.Message message, HumanMessageBase replyToMessage, string callSign, diff --git a/BotNet.Commands/ChatAggregate/Chat.cs b/BotNet.Commands/ChatAggregate/Chat.cs index f5ea7fd..b2b2562 100644 --- a/BotNet.Commands/ChatAggregate/Chat.cs +++ b/BotNet.Commands/ChatAggregate/Chat.cs @@ -3,26 +3,18 @@ using Telegram.Bot.Types.Enums; namespace BotNet.Commands.ChatAggregate { - public abstract record ChatBase { - public ChatId Id { get; } - public string? Title { get; } - - protected ChatBase( - ChatId id, - string? title - ) { - Id = id; - Title = title; - } - + public abstract record ChatBase( + ChatId Id, + string? Title + ) { public static bool TryCreate( Telegram.Bot.Types.Chat telegramChat, CommandPriorityCategorizer priorityCategorizer, [NotNullWhen(true)] out ChatBase? chat ) { chat = telegramChat switch { - Telegram.Bot.Types.Chat { Type: ChatType.Private } => PrivateChat.FromTelegramChat(telegramChat), - Telegram.Bot.Types.Chat { Type: ChatType.Group or ChatType.Supergroup } => priorityCategorizer.IsHomeGroup(telegramChat.Id) + { Type: ChatType.Private } => PrivateChat.FromTelegramChat(telegramChat), + { Type: ChatType.Group or ChatType.Supergroup } => priorityCategorizer.IsHomeGroup(telegramChat.Id) ? HomeGroupChat.FromTelegramChat(telegramChat) : GroupChat.FromTelegramChat(telegramChat), _ => null diff --git a/BotNet.Commands/CommandPrioritization/CommandPrioritizationOptions.cs b/BotNet.Commands/CommandPrioritization/CommandPrioritizationOptions.cs index d358d41..088035b 100644 --- a/BotNet.Commands/CommandPrioritization/CommandPrioritizationOptions.cs +++ b/BotNet.Commands/CommandPrioritization/CommandPrioritizationOptions.cs @@ -1,6 +1,6 @@ namespace BotNet.Commands.CommandPrioritization { public sealed class CommandPrioritizationOptions { - public string[] HomeGroupChatIds { get; set; } = []; - public string[] VIPUserIds { get; set; } = []; + public string[] HomeGroupChatIds { get; init; } = []; + public string[] VipUserIds { get; init; } = []; } } diff --git a/BotNet.Commands/CommandPrioritization/CommandPriorityCategorizer.cs b/BotNet.Commands/CommandPrioritization/CommandPriorityCategorizer.cs index dea68fb..f3e078d 100644 --- a/BotNet.Commands/CommandPrioritization/CommandPriorityCategorizer.cs +++ b/BotNet.Commands/CommandPrioritization/CommandPriorityCategorizer.cs @@ -17,7 +17,7 @@ IOptions optionsAccessor .Distinct() .ToImmutableHashSet(); - _vipUserIds = optionsAccessor.Value.VIPUserIds + _vipUserIds = optionsAccessor.Value.VipUserIds .SelectMany(userId => userId.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) .Select(userIdStr => long.TryParse(userIdStr, out long userId) ? (long?)userId : null) .Where(userId => userId.HasValue) @@ -30,7 +30,7 @@ public bool IsHomeGroup(long chatId) { return _homeGroupChatIds.Contains(chatId); } - public bool IsVIPUser(long userId) { + public bool IsVipUser(long userId) { return _vipUserIds.Contains(userId); } } diff --git a/BotNet.Commands/SQL/SQLCommand.cs b/BotNet.Commands/SQL/SQLCommand.cs index d6d844e..f379c4c 100644 --- a/BotNet.Commands/SQL/SQLCommand.cs +++ b/BotNet.Commands/SQL/SQLCommand.cs @@ -6,13 +6,13 @@ using SqlParser.Ast; namespace BotNet.Commands.SQL { - public sealed record SQLCommand : ICommand { + public sealed record SqlCommand : ICommand { public string RawStatement { get; } public Statement.Select SelectStatement { get; } - public MessageId SQLMessageId { get; } + public MessageId SqlMessageId { get; } public ChatBase Chat { get; } - private SQLCommand( + private SqlCommand( string rawStatement, Statement.Select selectStatement, MessageId sqlMessageId, @@ -20,14 +20,14 @@ ChatBase chat ) { RawStatement = rawStatement; SelectStatement = selectStatement; - SQLMessageId = sqlMessageId; + SqlMessageId = sqlMessageId; Chat = chat; } public static bool TryCreate( Telegram.Bot.Types.Message message, CommandPriorityCategorizer commandPriorityCategorizer, - [NotNullWhen(true)] out SQLCommand? sqlCommand + [NotNullWhen(true)] out SqlCommand? sqlCommand ) { // Must start with select if (message.Text is not { } text || !text.StartsWith("select", StringComparison.OrdinalIgnoreCase)) { @@ -44,7 +44,7 @@ public static bool TryCreate( // Must be a valid SQL statement Sequence ast; try { - ast = new SqlParser.Parser().ParseSql(text); + ast = new Parser().ParseSql(text); } catch { sqlCommand = null; return false; diff --git a/BotNet.Commands/SenderAggregate/Sender.cs b/BotNet.Commands/SenderAggregate/Sender.cs index eb97176..edd7aed 100644 --- a/BotNet.Commands/SenderAggregate/Sender.cs +++ b/BotNet.Commands/SenderAggregate/Sender.cs @@ -6,7 +6,7 @@ public abstract record SenderBase( SenderId Id, string Name ) { - public abstract string ChatGPTRole { get; } + public abstract string ChatGptRole { get; } public abstract string GeminiRole { get; } } @@ -14,7 +14,7 @@ public record HumanSender( SenderId Id, string Name ) : SenderBase(Id, Name) { - public override string ChatGPTRole => "user"; + public override string ChatGptRole => "user"; public override string GeminiRole => "user"; public static bool TryCreate( @@ -25,24 +25,26 @@ public static bool TryCreate( if (user is not { IsBot: false, Id: long senderId, - FirstName: string senderFirstName, + FirstName: { } senderFirstName, LastName: var senderLastName }) { humanSender = null; return false; } - if (commandPriorityCategorizer.IsVIPUser(senderId)) { - humanSender = new VIPSender( + if (commandPriorityCategorizer.IsVipUser(senderId)) { + humanSender = new VipSender( Id: senderId, - Name: senderLastName is { } ? $"{senderFirstName} {senderLastName}" : senderFirstName + Name: senderLastName is not null + ? $"{senderFirstName} {senderLastName}" : senderFirstName ); return true; } humanSender = new HumanSender( Id: senderId, - Name: senderLastName is { } ? $"{senderFirstName} {senderLastName}" : senderFirstName + Name: senderLastName is not null + ? $"{senderFirstName} {senderLastName}" : senderFirstName ); return true; } @@ -52,7 +54,7 @@ public sealed record BotSender( SenderId Id, string Name ) : SenderBase(Id, Name) { - public override string ChatGPTRole => "assistant"; + public override string ChatGptRole => "assistant"; public override string GeminiRole => "model"; public static bool TryCreate( @@ -62,12 +64,13 @@ public static bool TryCreate( if (user is { IsBot: true, Id: long senderId, - FirstName: string senderFirstName, + FirstName: { } senderFirstName, LastName: var senderLastName }) { botSender = new BotSender( Id: senderId, - Name: senderLastName is { } ? $"{senderFirstName} {senderLastName}" : senderFirstName + Name: senderLastName is not null + ? $"{senderFirstName} {senderLastName}" : senderFirstName ); return true; } @@ -77,7 +80,7 @@ public static bool TryCreate( } } - public sealed record VIPSender( + public sealed record VipSender( SenderId Id, string Name ) : HumanSender(Id, Name); diff --git a/BotNet.Services/BMKG/BMKG.cs b/BotNet.Services/BMKG/BMKG.cs index 193352e..3984052 100644 --- a/BotNet.Services/BMKG/BMKG.cs +++ b/BotNet.Services/BMKG/BMKG.cs @@ -1,12 +1,12 @@ using System.Net.Http; namespace BotNet.Services.BMKG { - public class BMKG { - protected string uriTemplate = "https://data.bmkg.go.id/DataMKG/TEWS/{0}.json"; - protected readonly HttpClient httpClient; + public class Bmkg { + protected const string UriTemplate = "https://data.bmkg.go.id/DataMKG/TEWS/{0}.json"; + protected readonly HttpClient HttpClient; - public BMKG(HttpClient client) { - httpClient = client; + protected Bmkg(HttpClient client) { + HttpClient = client; } } } diff --git a/BotNet.Services/BMKG/EarthQuake.cs b/BotNet.Services/BMKG/EarthQuake.cs index 21b458d..64b2806 100644 --- a/BotNet.Services/BMKG/EarthQuake.cs +++ b/BotNet.Services/BMKG/EarthQuake.cs @@ -1,9 +1,9 @@ namespace BotNet.Services.BMKG { public record EarthQuake { - public QuakeInfo InfoGempa { get; set; } = new QuakeInfo(); + public QuakeInfo InfoGempa { get; set; } = new(); public record QuakeInfo { - public Quake Gempa { get; set; } = new Quake(); + public Quake Gempa { get; set; } = new(); public record Quake { public string Tanggal { get; set; } = string.Empty; @@ -15,10 +15,7 @@ public record Quake { public string Potensi { get; set; } = string.Empty; public string Dirasakan { get; set; } = string.Empty; public string Shakemap{ get; set; } = string.Empty; - public string ShakemapUrl { - get => $"https://data.bmkg.go.id/DataMKG/TEWS/{Shakemap}"; - set { } - } + public string ShakemapUrl => $"https://data.bmkg.go.id/DataMKG/TEWS/{Shakemap}"; } } diff --git a/BotNet.Services/BMKG/LatestEarthQuake.cs b/BotNet.Services/BMKG/LatestEarthQuake.cs index 84c4e8b..09bcaee 100644 --- a/BotNet.Services/BMKG/LatestEarthQuake.cs +++ b/BotNet.Services/BMKG/LatestEarthQuake.cs @@ -4,13 +4,15 @@ using System.Threading.Tasks; namespace BotNet.Services.BMKG { - public class LatestEarthQuake(HttpClient client) : BMKG(client) { - private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { PropertyNameCaseInsensitive = true }; + public class LatestEarthQuake( + HttpClient client + ) : Bmkg(client) { + private static readonly JsonSerializerOptions JsonSerializerOptions = new() { PropertyNameCaseInsensitive = true }; public async Task<(string Text, string ShakemapUrl)> GetLatestAsync() { - string url = string.Format(uriTemplate, "autogempa"); + string url = string.Format(UriTemplate, "autogempa"); - HttpResponseMessage response = await httpClient.GetAsync(url); + HttpResponseMessage response = await HttpClient.GetAsync(url); response.EnsureSuccessStatusCode(); if (response.Content.Headers.ContentType!.MediaType is not "application/json") { @@ -19,27 +21,31 @@ public class LatestEarthQuake(HttpClient client) : BMKG(client) { Stream bodyContent = await response.Content.ReadAsStreamAsync(); - EarthQuake? bodyResponse = await JsonSerializer.DeserializeAsync(bodyContent, JSON_SERIALIZER_OPTIONS); + EarthQuake? bodyResponse = await JsonSerializer.DeserializeAsync(bodyContent, JsonSerializerOptions); if (bodyResponse is null) { throw new JsonException("Failed to parse body"); } - string textResult = "Gempa Terkini\n" - + $"Magnitudo: {bodyResponse.InfoGempa.Gempa.Magnitude}\n" - + $"Tanggal: {bodyResponse.InfoGempa.Gempa.Tanggal} {bodyResponse.InfoGempa.Gempa.Jam}\n" - + $"Koordinat: {bodyResponse.InfoGempa.Gempa.Coordinates}\n" - + $"Kedalaman: {bodyResponse.InfoGempa.Gempa.Kedalaman}\n" - + $"Wilayah: {bodyResponse.InfoGempa.Gempa.Wilayah}\n" - + $"Potensi: {bodyResponse.InfoGempa.Gempa.Potensi}\n" - + "\n\nJaga diri, keluarga dan orang tersayang anda"; + string textResult = $""" + Gempa Terkini + Magnitudo: {bodyResponse.InfoGempa.Gempa.Magnitude} + Tanggal: {bodyResponse.InfoGempa.Gempa.Tanggal} {bodyResponse.InfoGempa.Gempa.Jam} + Koordinat: {bodyResponse.InfoGempa.Gempa.Coordinates} + Kedalaman: {bodyResponse.InfoGempa.Gempa.Kedalaman} + Wilayah: {bodyResponse.InfoGempa.Gempa.Wilayah} + Potensi: {bodyResponse.InfoGempa.Gempa.Potensi} + + + Jaga diri, keluarga dan orang tersayang anda + """; string shakemapUrl = bodyResponse.InfoGempa.Gempa.ShakemapUrl; return ( Text: textResult, ShakemapUrl: shakemapUrl - ); + ); } } } diff --git a/BotNet.Services/BMKG/ServiceCollectionExtensions.cs b/BotNet.Services/BMKG/ServiceCollectionExtensions.cs index b64a3d5..30c2973 100644 --- a/BotNet.Services/BMKG/ServiceCollectionExtensions.cs +++ b/BotNet.Services/BMKG/ServiceCollectionExtensions.cs @@ -2,7 +2,7 @@ namespace BotNet.Services.BMKG { public static class ServiceCollectionExtensions { - public static IServiceCollection AddBMKG(this IServiceCollection services) { + public static IServiceCollection AddBmkg(this IServiceCollection services) { services.AddTransient(); return services; diff --git a/BotNet.Services/BotCommands/OpenAI.cs b/BotNet.Services/BotCommands/OpenAI.cs deleted file mode 100644 index d343ba4..0000000 --- a/BotNet.Services/BotCommands/OpenAI.cs +++ /dev/null @@ -1,576 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using BotNet.Services.OpenAI; -using BotNet.Services.OpenAI.Models; -using BotNet.Services.OpenAI.Skills; -using BotNet.Services.RateLimit; -using BotNet.Services.Stability.Models; -using BotNet.Services.Stability.Skills; -using Microsoft.Extensions.DependencyInjection; -using RG.Ninja; -using SkiaSharp; -using Telegram.Bot; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; -using Telegram.Bot.Types.ReplyMarkups; - -namespace BotNet.Services.BotCommands { - [Obsolete("Must be refactored to AI call commands")] - public static class OpenAI { - private static readonly RateLimiter CHAT_GROUP_RATE_LIMITER = RateLimiter.PerUserPerChat(5, TimeSpan.FromMinutes(15)); - private static readonly RateLimiter CHAT_PRIVATE_RATE_LIMITER = RateLimiter.PerUser(20, TimeSpan.FromMinutes(15)); - - public static async Task ChatWithSarcasticBotAsync(ITelegramBotClient botClient, IServiceProvider serviceProvider, Message message, string callSign, CancellationToken cancellationToken) { - if (message.Text!.StartsWith(callSign, out string? s) - && s.TrimStart() is string { Length: > 0 } chatMessage) { - try { - (message.Chat.Type == ChatType.Private - ? CHAT_PRIVATE_RATE_LIMITER - : CHAT_GROUP_RATE_LIMITER - ).ValidateActionRate(message.Chat.Id, message.From!.Id); - string result = await serviceProvider.GetRequiredService().ChatAsync( - callSign: callSign, - name: $"{message.From!.FirstName}{message.From.LastName?.Let(lastName => " " + lastName)}", - question: chatMessage, - cancellationToken: cancellationToken - ); - ImmutableList attachments = serviceProvider.GetRequiredService().GenerateAttachments(result); - if (attachments.Count == 0) { - return await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: WebUtility.HtmlEncode(result), - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } else if (attachments.Count == 1) { - return await botClient.SendPhotoAsync( - chatId: message.Chat.Id, - photo: new InputFileUrl(attachments[0]), - caption: WebUtility.HtmlEncode(result), - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } else { - Message sentMessage = await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: WebUtility.HtmlEncode(result), - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - await botClient.SendMediaGroupAsync( - chatId: message.Chat.Id, - media: from attachment in attachments - select new InputMediaPhoto(new InputFileUrl(attachment.OriginalString)), - cancellationToken: cancellationToken); - return sentMessage; - } - } catch (RateLimitExceededException exc) when (exc is { Cooldown: var cooldown }) { - if (message.Chat.Type == ChatType.Private) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: $"Anda terlalu banyak memanggil Pakde. Coba lagi {cooldown}.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } else { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: $"Anda terlalu banyak memanggil Pakde di sini. Coba lagi {cooldown} atau lanjutkan di private chat.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - replyMarkup: new InlineKeyboardMarkup( - InlineKeyboardButton.WithUrl("Private chat 💬", "t.me/TeknumBot") - ), - cancellationToken: cancellationToken); - } - } catch (OperationCanceledException) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Timeout exceeded.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } - } - return null; - } - - public static async Task ChatWithSarcasticBotAsync(ITelegramBotClient botClient, IServiceProvider serviceProvider, Message message, ImmutableList<(string Sender, string? Text, string? ImageBase64)> thread, string callSign, CancellationToken cancellationToken) { - try { - (message.Chat.Type == ChatType.Private - ? CHAT_PRIVATE_RATE_LIMITER - : CHAT_GROUP_RATE_LIMITER - ).ValidateActionRate(message.Chat.Id, message.From!.Id); - string result = await serviceProvider.GetRequiredService().RespondToThreadAsync( - callSign: callSign, - name: $"{message.From!.FirstName}{message.From.LastName?.Let(lastName => " " + lastName)}", - question: message.Text!, - thread: thread, - cancellationToken: cancellationToken - ); - ImmutableList attachments = serviceProvider.GetRequiredService().GenerateAttachments(result); - if (attachments.Count == 0) { - return await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: WebUtility.HtmlEncode(result), - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } else if (attachments.Count == 1) { - return await botClient.SendPhotoAsync( - chatId: message.Chat.Id, - photo: new InputFileUrl(attachments[0]), - caption: WebUtility.HtmlEncode(result), - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } else { - Message sentMessage = await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: WebUtility.HtmlEncode(result), - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - await botClient.SendMediaGroupAsync( - chatId: message.Chat.Id, - media: from attachment in attachments - select new InputMediaPhoto(new InputFileUrl(attachment.OriginalString)), - cancellationToken: cancellationToken); - return sentMessage; - } - } catch (RateLimitExceededException exc) when (exc is { Cooldown: var cooldown }) { - if (message.Chat.Type == ChatType.Private) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: $"Anda terlalu banyak memanggil Pakde. Coba lagi {cooldown}.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } else { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: $"Anda terlalu banyak memanggil Pakde di sini. Coba lagi {cooldown} atau lanjutkan di private chat.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - replyMarkup: new InlineKeyboardMarkup( - InlineKeyboardButton.WithUrl("Private chat 💬", "t.me/TeknumBot") - ), - cancellationToken: cancellationToken); - } - } catch (OperationCanceledException) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Timeout exceeded.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } - return null; - } - - private static readonly RateLimiter IMAGE_GENERATION_PER_USER_RATE_LIMITER = RateLimiter.PerUser(1, TimeSpan.FromMinutes(5)); - private static readonly RateLimiter IMAGE_GENERATION_PER_CHAT_RATE_LIMITER = RateLimiter.PerChat(2, TimeSpan.FromMinutes(3)); - public static async Task StreamChatWithFriendlyBotAsync( - ITelegramBotClient botClient, - IServiceProvider serviceProvider, - Message message, - CancellationToken cancellationToken - ) { - try { - (message.Chat.Type == ChatType.Private - ? CHAT_PRIVATE_RATE_LIMITER - : CHAT_GROUP_RATE_LIMITER - ).ValidateActionRate(message.Chat.Id, message.From!.Id); - - string? fileId; - string? prompt; - if (message is { Photo.Length: > 0, Caption: { } }) { - fileId = message.Photo.OrderByDescending(photoSize => photoSize.Width).First().FileId; - prompt = message.Caption; - } else if (message.ReplyToMessage is { Photo.Length: > 0 } - && message.Text is { }) { - fileId = message.ReplyToMessage.Photo.OrderByDescending(photoSize => photoSize.Width).First().FileId; - prompt = message.Text; - } else if (message.ReplyToMessage is { Sticker: { } } - && message.Text is { }) { - fileId = message.ReplyToMessage.Sticker.FileId; - prompt = message.Text; - } else { - fileId = null; - prompt = null; - } - - if (fileId != null && prompt != null) { - (string? imageBase64, string? error) = await GetImageBase64Async(botClient, fileId, cancellationToken); - if (error != null) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: $"{error}", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - return; - } - - await serviceProvider.GetRequiredService().StreamChatAsync( - message: prompt, - imageBase64: imageBase64!, - chatId: message.Chat.Id, - replyToMessageId: message.MessageId - ); - } else { - IntentDetector intentDetector = serviceProvider.GetRequiredService(); - ChatIntent chatIntent = await intentDetector.DetectChatIntentAsync( - message: message.Text!, - cancellationToken: cancellationToken - ); - - switch (chatIntent) { - case ChatIntent.Question: - await serviceProvider.GetRequiredService().StreamChatAsync( - message: message.Text!, - chatId: message.Chat.Id, - replyToMessageId: message.MessageId - ); - break; - case ChatIntent.ImageGeneration: { - IMAGE_GENERATION_PER_USER_RATE_LIMITER.ValidateActionRate( - chatId: message.Chat.Id, - userId: message.From.Id - ); - IMAGE_GENERATION_PER_CHAT_RATE_LIMITER.ValidateActionRate( - chatId: message.Chat.Id, - userId: message.From.Id - ); - Message busyMessage = await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Generating image… ⏳", - parseMode: ParseMode.Markdown, - messageThreadId: message.MessageId, - cancellationToken: cancellationToken - ); - //Uri generatedImageUrl = await serviceProvider.GetRequiredService().GenerateImageAsync( - // prompt: message.Text!, - // cancellationToken: cancellationToken - //); - try { - byte[] generatedImage = await serviceProvider.GetRequiredService().GenerateImageAsync( - prompt: message.Text!, - cancellationToken: cancellationToken - ); - using MemoryStream generatedImageStream = new(generatedImage); - try { - await botClient.DeleteMessageAsync( - chatId: busyMessage.Chat.Id, - messageId: busyMessage.MessageId, - cancellationToken: cancellationToken - ); - } catch (OperationCanceledException) { - throw; - } - - Message generatedImageMessage = await botClient.SendPhotoAsync( - chatId: message.Chat.Id, - photo: new InputFileStream(generatedImageStream, "art.png"), - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken - ); - - // Track generated image for continuation - serviceProvider.GetRequiredService().TrackMessage( - messageId: generatedImageMessage.MessageId, - sender: "GPT", - text: null, - imageBase64: Convert.ToBase64String(generatedImage), - replyToMessageId: message.MessageId - ); - } catch (ContentFilteredException exc) { - await botClient.EditMessageTextAsync( - chatId: busyMessage.Chat.Id, - messageId: busyMessage.MessageId, - text: $"{exc.Message ?? "Content filtered."}", - parseMode: ParseMode.Html - ); - } catch { - await botClient.EditMessageTextAsync( - chatId: busyMessage.Chat.Id, - messageId: busyMessage.MessageId, - text: "Failed to generate image.", - parseMode: ParseMode.Html - ); - } - break; - } - } - } - } catch (RateLimitExceededException exc) when (exc is { Cooldown: var cooldown }) { - if (message.Chat.Type == ChatType.Private) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: $"Anda terlalu banyak memanggil AI. Coba lagi {cooldown}.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } else { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: $"Anda terlalu banyak memanggil AI di sini. Coba lagi {cooldown} atau lanjutkan di private chat.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - replyMarkup: new InlineKeyboardMarkup( - InlineKeyboardButton.WithUrl("Private chat 💬", "t.me/TeknumBot") - ), - cancellationToken: cancellationToken); - } - } catch (HttpRequestException exc) when (exc.StatusCode == HttpStatusCode.TooManyRequests) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Too many requests.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } catch (HttpRequestException) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Unknown error.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } catch (OperationCanceledException) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Timeout exceeded.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } - } - - public static async Task StreamChatWithFriendlyBotAsync(ITelegramBotClient botClient, IServiceProvider serviceProvider, Message message, ImmutableList<(string Sender, string? Text, string? ImageBase64)> thread, CancellationToken cancellationToken) { - try { - (message.Chat.Type == ChatType.Private - ? CHAT_PRIVATE_RATE_LIMITER - : CHAT_GROUP_RATE_LIMITER - ).ValidateActionRate(message.Chat.Id, message.From!.Id); - - if (thread.FirstOrDefault().ImageBase64 is { } imageBase64) { - IntentDetector intentDetector = serviceProvider.GetRequiredService(); - ImagePromptIntent imagePromptIntent = await intentDetector.DetectImagePromptIntentAsync( - message: message.Text!, - cancellationToken: cancellationToken - ); - - switch (imagePromptIntent) { - case ImagePromptIntent.ImageVariation: - IMAGE_GENERATION_PER_USER_RATE_LIMITER.ValidateActionRate( - chatId: message.Chat.Id, - userId: message.From.Id - ); - IMAGE_GENERATION_PER_CHAT_RATE_LIMITER.ValidateActionRate( - chatId: message.Chat.Id, - userId: message.From.Id - ); - Message busyMessage = await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Modifying image… ⏳", - parseMode: ParseMode.Markdown, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken - ); - try { - byte[] promptImage = Convert.FromBase64String(imageBase64); - byte[] modifiedImage = await serviceProvider.GetRequiredService().ModifyImageAsync( - image: promptImage, - prompt: message.Text!, - cancellationToken - ); - using MemoryStream modifiedImageStream = new(modifiedImage); - try { - await botClient.DeleteMessageAsync( - chatId: busyMessage.Chat.Id, - messageId: busyMessage.MessageId, - cancellationToken: cancellationToken - ); - } catch (OperationCanceledException) { - throw; - } - - Message modifiedImageMessage = await botClient.SendPhotoAsync( - chatId: message.Chat.Id, - photo: new InputFileStream(modifiedImageStream, "art.png"), - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken - ); - - // Track generated image for continuation - serviceProvider.GetRequiredService().TrackMessage( - messageId: modifiedImageMessage.MessageId, - sender: "GPT", - text: null, - imageBase64: Convert.ToBase64String(modifiedImage), - replyToMessageId: message.MessageId - ); - } catch (ContentFilteredException exc) { - await botClient.EditMessageTextAsync( - chatId: busyMessage.Chat.Id, - messageId: busyMessage.MessageId, - text: $"{exc.Message ?? "Content filtered."}", - parseMode: ParseMode.Html - ); - } catch { - await botClient.EditMessageTextAsync( - chatId: busyMessage.Chat.Id, - messageId: busyMessage.MessageId, - text: "Failed to modify image.", - parseMode: ParseMode.Html - ); - } - break; - case ImagePromptIntent.Vision: - await serviceProvider.GetRequiredService().StreamChatAsync( - message: message.Text!, - imageBase64: imageBase64, - chatId: message.Chat.Id, - replyToMessageId: message.MessageId - ); - break; - } - } else { - await serviceProvider.GetRequiredService().StreamChatAsync( - message: message.Text!, - thread: thread, - chatId: message.Chat.Id, - replyToMessageId: message.MessageId - ); - } - } catch (RateLimitExceededException exc) when (exc is { Cooldown: var cooldown }) { - if (message.Chat.Type == ChatType.Private) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: $"Anda terlalu banyak memanggil AI. Coba lagi {cooldown}.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } else { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: $"Anda terlalu banyak memanggil AI di sini. Coba lagi {cooldown} atau lanjutkan di private chat.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - replyMarkup: new InlineKeyboardMarkup( - InlineKeyboardButton.WithUrl("Private chat 💬", "t.me/TeknumBot") - ), - cancellationToken: cancellationToken); - } - } catch (OperationCanceledException) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Timeout exceeded.", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - } - } - - private static async Task<(string? ImageBase64, string? Error)> GetImageBase64Async(ITelegramBotClient botClient, string fileId, CancellationToken cancellationToken) { - // Download photo - using MemoryStream originalImageStream = new(); - await botClient.GetInfoAndDownloadFile( - fileId: fileId, - destination: originalImageStream, - cancellationToken: cancellationToken); - byte[] originalImage = originalImageStream.ToArray(); - - // Limit input image to 300KB - if (originalImage.Length > 300 * 1024) { - return (null, "Image larger than 300KB"); - } - - // Decode image - originalImageStream.Position = 0; - using SKCodec codec = SKCodec.Create(originalImageStream, out SKCodecResult codecResult); - if (codecResult != SKCodecResult.Success) { - return (null, "Invalid image"); - } - - if (codec.EncodedFormat != SKEncodedImageFormat.Jpeg - && codec.EncodedFormat != SKEncodedImageFormat.Webp) { - return (null, "Image must be compressed image"); - } - SKBitmap bitmap = SKBitmap.Decode(codec); - - // Limit input image to 1280x1280 - if (bitmap.Width > 1280 || bitmap.Width > 1280) { - return (null, "Image larger than 1280x1280"); - } - - // Handle stickers - if (codec.EncodedFormat == SKEncodedImageFormat.Webp) { - SKImage image = SKImage.FromBitmap(bitmap); - SKData data = image.Encode(SKEncodedImageFormat.Jpeg, 20); - using MemoryStream jpegStream = new(); - data.SaveTo(jpegStream); - - // Encode image as base64 - return (Convert.ToBase64String(jpegStream.ToArray()), null); - } - - // Encode image as base64 - return (Convert.ToBase64String(originalImage), null); - } - } -} diff --git a/BotNet.Services/BotCommands/Preview.cs b/BotNet.Services/BotCommands/Preview.cs deleted file mode 100644 index fdbba8a..0000000 --- a/BotNet.Services/BotCommands/Preview.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using BotNet.Services.Preview; -using Microsoft.Extensions.DependencyInjection; -using Telegram.Bot; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; - -namespace BotNet.Services.BotCommands { - [Obsolete("Should be refactored to PreviewCommandHandler later")] - public static class Preview { - public static async Task GetPreviewAsync(ITelegramBotClient botClient, IServiceProvider serviceProvider, Message message, CancellationToken cancellationToken) { - if (message.Entities?.FirstOrDefault() is { Type: MessageEntityType.BotCommand, Offset: 0, Length: int commandLength } - && message.Text![commandLength..].Trim() is string commandArgument) { - - if (commandArgument.Length <= 0 && message.ReplyToMessage?.Text is null) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Gunakan /preview youtoube link atau reply message dengan command /preview", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - - return; - } - - Uri? youtubeLink; - Uri? previewYoutubeStoryboard; - - if (message.ReplyToMessage?.Text is string repliedToMessage) { - youtubeLink = YoutubePreview.ValidateYoutubeLink(repliedToMessage); - if (youtubeLink is null) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Youtube link tidak valid", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - - return; - } - - previewYoutubeStoryboard = await serviceProvider.GetRequiredService().YoutubeStoryBoardAsync(youtubeLink, cancellationToken); - - await botClient.SendPhotoAsync( - chatId: message.Chat.Id, - photo: new InputFileUrl(previewYoutubeStoryboard), - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - parseMode: ParseMode.Html, - cancellationToken: cancellationToken); - } else if (commandArgument.Length >= 0) { - youtubeLink = YoutubePreview.ValidateYoutubeLink(commandArgument); - if (youtubeLink is null) { - await botClient.SendTextMessageAsync( - chatId: message.Chat.Id, - text: "Youtube link tidak valid", - parseMode: ParseMode.Html, - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - cancellationToken: cancellationToken); - - return; - } - - previewYoutubeStoryboard = await serviceProvider.GetRequiredService().YoutubeStoryBoardAsync(youtubeLink, cancellationToken); - - await botClient.SendPhotoAsync( - chatId: message.Chat.Id, - photo: new InputFileUrl(previewYoutubeStoryboard), - replyParameters: new ReplyParameters { - MessageId = message.MessageId - }, - parseMode: ParseMode.Html, - cancellationToken: cancellationToken); - } - } - } - } -} diff --git a/BotNet.Services/BotProfile/BotProfileAccessor.cs b/BotNet.Services/BotProfile/BotProfileAccessor.cs index 00465a0..f672a76 100644 --- a/BotNet.Services/BotProfile/BotProfileAccessor.cs +++ b/BotNet.Services/BotProfile/BotProfileAccessor.cs @@ -7,12 +7,10 @@ namespace BotNet.Services.BotProfile { public sealed class BotProfileAccessor( ITelegramBotClient telegramBotClient ) { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private User? _me; public async Task GetBotProfileAsync(CancellationToken cancellationToken) { - _me ??= await _telegramBotClient.GetMe(cancellationToken); + _me ??= await telegramBotClient.GetMe(cancellationToken); return _me; } } diff --git a/BotNet.Services/Brainfuck/BrainfuckInterpreter.cs b/BotNet.Services/Brainfuck/BrainfuckInterpreter.cs index 70b30c0..83462a2 100644 --- a/BotNet.Services/Brainfuck/BrainfuckInterpreter.cs +++ b/BotNet.Services/Brainfuck/BrainfuckInterpreter.cs @@ -6,9 +6,10 @@ namespace BotNet.Services.Brainfuck { public static class BrainfuckInterpreter { public static string RunBrainfuck(string code) { - byte[] program = Encoding.UTF8.GetBytes(code); + Span program = stackalloc byte[Encoding.UTF8.GetByteCount(code)]; + Encoding.UTF8.GetBytes(code, program); int programPointer = 0; - byte[] memory = new byte[1024]; + Span memory = stackalloc byte[1024]; int pointer = 0; Stack loopPointers = new(); Dictionary loopCache = new(); diff --git a/BotNet.Services/Brainfuck/BrainfuckTranspiler.cs b/BotNet.Services/Brainfuck/BrainfuckTranspiler.cs index 06c0118..0df2175 100644 --- a/BotNet.Services/Brainfuck/BrainfuckTranspiler.cs +++ b/BotNet.Services/Brainfuck/BrainfuckTranspiler.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; namespace BotNet.Services.Brainfuck { public class BrainfuckTranspiler { @@ -36,7 +37,17 @@ public string TranspileBrainfuck(string message) { return Generate(message); } - private static int GCD(int c, int a) => a == 0 ? c : GCD(a, c % a); + private static int Gcd( + int c, + int a + ) { + while (true) { + if (a == 0) return c; + int c1 = c; + c = a; + a = c1 % a; + } + } private static int InverseMod(int c, int a) { int f = 1, d = 0, b; @@ -65,7 +76,7 @@ private void Next() { for (int c = 0; 256 > c; c++) { for (int a = 1; 40 > a; a++) { for (int f = InverseMod(a, 256) & 255, d = 1; 40 > d; d++) { - if (1 == GCD(a, d)) { + if (1 == Gcd(a, d)) { int b; int e; if ((a & 1) != 0) { @@ -118,16 +129,17 @@ private void Next() { } private string Generate(string s) { - byte[] c = Encoding.UTF8.GetBytes(s); - string d = ""; + Span c = stackalloc byte[Encoding.UTF8.GetByteCount(s)]; + Encoding.UTF8.GetBytes(s, c); + StringBuilder d = new(); for (int a = 0, f = c.Length, b = 0; b < f; b++) { int e = c[b] & 255; - string[] l = { ">" + _map[0][e], _map[a][e] }; + string[] l = [$">{_map[0][e]}", _map[a][e]]; int g = ShortestStr(l); - d += l[g] + "."; + d.Append(l[g]).Append('.'); a = e; } - return d; + return d.ToString(); } } } diff --git a/BotNet.Services/BubbleWrap/BubbleWrapKeyboardGenerator.cs b/BotNet.Services/BubbleWrap/BubbleWrapKeyboardGenerator.cs index f332a3d..9813e66 100644 --- a/BotNet.Services/BubbleWrap/BubbleWrapKeyboardGenerator.cs +++ b/BotNet.Services/BubbleWrap/BubbleWrapKeyboardGenerator.cs @@ -6,28 +6,27 @@ namespace BotNet.Services.BubbleWrap { public sealed class BubbleWrapKeyboardGenerator( IMemoryCache memoryCache ) { - public static readonly InlineKeyboardMarkup EMPTY_KEYBOARD = BubbleWrapSheet.EmptySheet.ToKeyboardMarkup(); - private readonly IMemoryCache _memoryCache = memoryCache; + public static readonly InlineKeyboardMarkup EmptyKeyboard = BubbleWrapSheet.EmptySheet.ToKeyboardMarkup(); public InlineKeyboardMarkup HandleCallback(long chatId, int messageId, string sheetData) { BubbleWrapId id = new(chatId, messageId); BubbleWrapSheet expectedSheet = BubbleWrapSheet.ParseSheetData(sheetData); - if (_memoryCache.TryGetValue(id, out BubbleWrapSheet? cachedSheet)) { + if (memoryCache.TryGetValue(id, out BubbleWrapSheet? cachedSheet)) { cachedSheet = cachedSheet!.CombineWith(expectedSheet); - _memoryCache.Set( + memoryCache.Set( key: id, value: cachedSheet, absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(1) ); return cachedSheet.ToKeyboardMarkup(); - } else { - _memoryCache.Set( - key: id, - value: expectedSheet, - absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(1) - ); - return expectedSheet.ToKeyboardMarkup(); } + + memoryCache.Set( + key: id, + value: expectedSheet, + absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(1) + ); + return expectedSheet.ToKeyboardMarkup(); } private readonly record struct BubbleWrapId( diff --git a/BotNet.Services/BubbleWrap/BubbleWrapSheet.cs b/BotNet.Services/BubbleWrap/BubbleWrapSheet.cs index 6c285e3..ce07667 100644 --- a/BotNet.Services/BubbleWrap/BubbleWrapSheet.cs +++ b/BotNet.Services/BubbleWrap/BubbleWrapSheet.cs @@ -29,7 +29,7 @@ public static BubbleWrapSheet ParseSheetData(string sheetData = "FFFF") { return new(data); } - public BubbleWrapSheet Pop(int row, int col) { + private BubbleWrapSheet Pop(int row, int col) { if (!Data[row, col]) return this; bool[,] data = (bool[,])Data.Clone(); @@ -47,7 +47,7 @@ public BubbleWrapSheet CombineWith(BubbleWrapSheet expectedSheet) { return new(data); } - public string ToSheetData() { + private string ToSheetData() { StringBuilder callbackData = new(); for (int row = 0; row < 4; row++) { int bitmap = 0; diff --git a/BotNet.Services/ChineseCalendar/ChineseCalendarScraper.cs b/BotNet.Services/ChineseCalendar/ChineseCalendarScraper.cs index 15575bf..a4b5569 100644 --- a/BotNet.Services/ChineseCalendar/ChineseCalendarScraper.cs +++ b/BotNet.Services/ChineseCalendar/ChineseCalendarScraper.cs @@ -10,8 +10,7 @@ namespace BotNet.Services.ChineseCalendar { public class ChineseCalendarScraper(HttpClient httpClient) { - private const string URL_TEMPLATE = "https://www.chinesecalendaronline.com/{0}/{1}/{2}.htm"; - private readonly HttpClient _httpClient = httpClient; + private const string UrlTemplate = "https://www.chinesecalendaronline.com/{0}/{1}/{2}.htm"; public async Task<( string Clash, @@ -22,9 +21,9 @@ public class ChineseCalendarScraper(HttpClient httpClient) { string[] AuspiciousActivities, string[] InauspiciousActivities )> GetYellowCalendarAsync(DateOnly date, CancellationToken cancellationToken) { - string url = string.Format(URL_TEMPLATE, date.Year, date.Month, date.Day); + string url = string.Format(UrlTemplate, date.Year, date.Month, date.Day); using HttpRequestMessage httpRequest = new(HttpMethod.Get, url); - using HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + using HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken); httpResponse.EnsureSuccessStatusCode(); string html = await httpResponse.Content.ReadAsStringAsync(cancellationToken); diff --git a/BotNet.Services/ClearScript/JsonConverters/ScriptObjectConverter.cs b/BotNet.Services/ClearScript/JsonConverters/ScriptObjectConverter.cs index 64c2c26..61fe985 100644 --- a/BotNet.Services/ClearScript/JsonConverters/ScriptObjectConverter.cs +++ b/BotNet.Services/ClearScript/JsonConverters/ScriptObjectConverter.cs @@ -8,7 +8,7 @@ namespace BotNet.Services.ClearScript.JsonConverters { public class ScriptObjectConverter : JsonConverter { public override bool CanConvert(Type typeToConvert) => typeof(ScriptObject).IsAssignableFrom(typeToConvert); - public override ScriptObject? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + public override ScriptObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); public override void Write(Utf8JsonWriter writer, ScriptObject value, JsonSerializerOptions options) { if (value is IList) { diff --git a/BotNet.Services/ClearScript/JsonConverters/UndefinedConverter.cs b/BotNet.Services/ClearScript/JsonConverters/UndefinedConverter.cs index d64e294..9e11a5c 100644 --- a/BotNet.Services/ClearScript/JsonConverters/UndefinedConverter.cs +++ b/BotNet.Services/ClearScript/JsonConverters/UndefinedConverter.cs @@ -5,7 +5,7 @@ namespace BotNet.Services.ClearScript.JsonConverters { public class UndefinedConverter : JsonConverter { - public override Undefined? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + public override Undefined Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); public override void Write(Utf8JsonWriter writer, Undefined value, JsonSerializerOptions options) { writer.WriteRawValue("undefined", skipInputValidation: true); diff --git a/BotNet.Services/ClearScript/V8Evaluator.cs b/BotNet.Services/ClearScript/V8Evaluator.cs index 8885493..174c0f0 100644 --- a/BotNet.Services/ClearScript/V8Evaluator.cs +++ b/BotNet.Services/ClearScript/V8Evaluator.cs @@ -7,8 +7,10 @@ using Microsoft.Extensions.Options; namespace BotNet.Services.ClearScript { - public class V8Evaluator { - private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { + public class V8Evaluator( + IOptions v8OptionsAccessor + ) { + private static readonly JsonSerializerOptions JsonSerializerOptions = new() { Converters = { new ScriptObjectConverter(), new DoubleConverter(), @@ -16,26 +18,19 @@ public class V8Evaluator { new UndefinedConverter() } }; - private readonly V8Options _v8Options; - - public V8Evaluator( - IOptions v8OptionsAccessor - ) { - _v8Options = v8OptionsAccessor.Value; - } + private readonly V8Options _v8Options = v8OptionsAccessor.Value; public async Task EvaluateAsync(string script, CancellationToken cancellationToken) { - using V8ScriptEngine engine = new() { - MaxRuntimeHeapSize = _v8Options.HeapSize, - MaxRuntimeStackUsage = _v8Options.StackUsage - }; + using V8ScriptEngine engine = new(); + engine.MaxRuntimeHeapSize = _v8Options.HeapSize; + engine.MaxRuntimeStackUsage = _v8Options.StackUsage; using CancellationTokenSource timeoutSource = new(TimeSpan.FromSeconds(1)); timeoutSource.Token.Register(engine.Interrupt); cancellationToken.Register(engine.Interrupt); return await Task.Run(() => { object? result = engine.Evaluate(script); - return JsonSerializer.Serialize(result, JSON_SERIALIZER_OPTIONS); - }); + return JsonSerializer.Serialize(result, JsonSerializerOptions); + }, timeoutSource.Token); } } } diff --git a/BotNet.Services/ClearScript/V8Options.cs b/BotNet.Services/ClearScript/V8Options.cs index a5b801a..c3ca29f 100644 --- a/BotNet.Services/ClearScript/V8Options.cs +++ b/BotNet.Services/ClearScript/V8Options.cs @@ -1,6 +1,6 @@ namespace BotNet.Services.ClearScript { public class V8Options { - public nuint HeapSize { get; set; } - public nuint StackUsage { get; set; } + public nuint HeapSize { get; init; } + public nuint StackUsage { get; init; } } } diff --git a/BotNet.Services/ColorCard/ColorCardRenderer.cs b/BotNet.Services/ColorCard/ColorCardRenderer.cs index a65bb9d..d13a74a 100644 --- a/BotNet.Services/ColorCard/ColorCardRenderer.cs +++ b/BotNet.Services/ColorCard/ColorCardRenderer.cs @@ -5,15 +5,9 @@ using SkiaSharp; namespace BotNet.Services.ColorCard { - public class ColorCardRenderer { - private readonly BotNetFontService _botNetFontService; - - public ColorCardRenderer( - BotNetFontService botNetFontService - ) { - _botNetFontService = botNetFontService; - } - + public class ColorCardRenderer( + BotNetFontService botNetFontService + ) { public byte[] RenderColorCard(string colorName) { if (string.IsNullOrWhiteSpace(colorName)) throw new ArgumentNullException(nameof(colorName)); @@ -45,15 +39,14 @@ public byte[] RenderColorCard(string colorName) { canvas.Clear(fillColor); - using Stream fontStream = _botNetFontService.GetFontStyleById("JetBrainsMonoNL-Regular").OpenStream(); + using Stream fontStream = botNetFontService.GetFontStyleById("JetBrainsMonoNL-Regular").OpenStream(); using SKTypeface typeface = SKTypeface.FromStream(fontStream); - using SKPaint paint = new() { - TextAlign = SKTextAlign.Center, - Color = textColor, - Typeface = typeface, - TextSize = 50f, - IsAntialias = true - }; + using SKPaint paint = new(); + paint.TextAlign = SKTextAlign.Center; + paint.Color = textColor; + paint.Typeface = typeface; + paint.TextSize = 50f; + paint.IsAntialias = true; SKRect textBound = new(); paint.MeasureText(normalizedName, ref textBound); canvas.DrawText( diff --git a/BotNet.Services/CopyPasta/CopyPastaLookup.cs b/BotNet.Services/CopyPasta/CopyPastaLookup.cs index 7145280..4c347d4 100644 --- a/BotNet.Services/CopyPasta/CopyPastaLookup.cs +++ b/BotNet.Services/CopyPasta/CopyPastaLookup.cs @@ -6,17 +6,17 @@ namespace BotNet.Services.CopyPasta { public class CopyPastaLookup { - private static readonly ImmutableDictionary> DICTIONARY; + private static readonly ImmutableDictionary> Dictionary; static CopyPastaLookup() { using Stream stream = Assembly.GetAssembly(typeof(CopyPastaLookup))!.GetManifestResourceStream("BotNet.Services.CopyPasta.Pasta.json")!; using StreamReader streamReader = new(stream); string json = streamReader.ReadToEnd(); - DICTIONARY = JsonSerializer.Deserialize>>(json)!; + Dictionary = JsonSerializer.Deserialize>>(json)!; } public static bool TryGetAutoText(string key, [NotNullWhen(true)] out ImmutableList? values) { - return DICTIONARY.TryGetValue(key, out values); + return Dictionary.TryGetValue(key, out values); } } } diff --git a/BotNet.Services/Craiyon/CraiyonClient.cs b/BotNet.Services/Craiyon/CraiyonClient.cs index dead521..440cade 100644 --- a/BotNet.Services/Craiyon/CraiyonClient.cs +++ b/BotNet.Services/Craiyon/CraiyonClient.cs @@ -8,39 +8,35 @@ using BotNet.Services.Craiyon.Models; namespace BotNet.Services.Craiyon { - public class CraiyonClient { - private const string URL = "https://backend.craiyon.com/generate"; - private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { + public class CraiyonClient( + HttpClient httpClient + ) { + private const string Url = "https://backend.craiyon.com/generate"; + private static readonly JsonSerializerOptions JsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - private readonly HttpClient _httpClient; - - public CraiyonClient( - HttpClient httpClient - ) { - _httpClient = httpClient; - } public async Task> GenerateImagesAsync(string prompt, CancellationToken cancellationToken) { - using HttpRequestMessage request = new(HttpMethod.Post, URL) { - Content = JsonContent.Create( - inputValue: new { - Prompt = prompt - }, - options: JSON_SERIALIZER_OPTIONS - ) - }; - using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); + using HttpRequestMessage request = new(HttpMethod.Post, Url); + request.Content = JsonContent.Create( + inputValue: new { + Prompt = prompt + }, + options: JsonSerializerOptions + ); + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); - ImagesResult? imagesResult = await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, cancellationToken); + ImagesResult? imagesResult = await response.Content.ReadFromJsonAsync(JsonSerializerOptions, cancellationToken); List images = new(); - if (imagesResult != null) { - foreach (string encodedImage in imagesResult.Images) { - byte[] image = Convert.FromBase64String(encodedImage.Replace("\\n", "")); - images.Add(image); - } + if (imagesResult == null) { + return images; + } + + foreach (string encodedImage in imagesResult.Images) { + byte[] image = Convert.FromBase64String(encodedImage.Replace("\\n", "")); + images.Add(image); } return images; } diff --git a/BotNet.Services/DynamicExpresso/CSharpEvaluator.cs b/BotNet.Services/DynamicExpresso/CSharpEvaluator.cs index 1a11a41..ad26998 100644 --- a/BotNet.Services/DynamicExpresso/CSharpEvaluator.cs +++ b/BotNet.Services/DynamicExpresso/CSharpEvaluator.cs @@ -1,17 +1,11 @@ using DynamicExpresso; namespace BotNet.Services.DynamicExpresso { - public class CSharpEvaluator { - private readonly Interpreter _interpreter; - - public CSharpEvaluator( - Interpreter interpreter - ) { - _interpreter = interpreter; - } - + public class CSharpEvaluator( + Interpreter interpreter + ) { public object Evaluate(string expression) { - return _interpreter.Eval(expression); + return interpreter.Eval(expression); } } } diff --git a/BotNet.Services/DynamicExpresso/ServiceCollectionExtensions.cs b/BotNet.Services/DynamicExpresso/ServiceCollectionExtensions.cs index 4ee88d7..973047d 100644 --- a/BotNet.Services/DynamicExpresso/ServiceCollectionExtensions.cs +++ b/BotNet.Services/DynamicExpresso/ServiceCollectionExtensions.cs @@ -1,10 +1,10 @@ -using System.Linq; -using DynamicExpresso; -using Microsoft.Extensions.DependencyInjection; +using System.Linq; +using DynamicExpresso; +using Microsoft.Extensions.DependencyInjection; -namespace BotNet.Services.DynamicExpresso { - public static class ServiceCollectionExtensions { - public static IServiceCollection AddCSharpEvaluator(this IServiceCollection services) { +namespace BotNet.Services.DynamicExpresso { + public static class ServiceCollectionExtensions { + public static IServiceCollection AddCSharpEvaluator(this IServiceCollection services) { services.AddSingleton(new Interpreter(options: InterpreterOptions.Default | InterpreterOptions.LambdaExpressions) .Reference( from type in new[] { @@ -80,9 +80,9 @@ from type in new[] { } select new ReferenceType(type) ) - ); - services.AddTransient(); - return services; - } - } -} + ); + services.AddTransient(); + return services; + } + } +} diff --git a/BotNet.Services/FancyText/FancyTextGenerator.cs b/BotNet.Services/FancyText/FancyTextGenerator.cs index 5a8613b..1ba2567 100644 --- a/BotNet.Services/FancyText/FancyTextGenerator.cs +++ b/BotNet.Services/FancyText/FancyTextGenerator.cs @@ -7,27 +7,26 @@ using System.Threading.Tasks; namespace BotNet.Services.FancyText { - public class FancyTextGenerator { - private static readonly Dictionary> CHAR_MAP_BY_STYLE = new(); - private static readonly SemaphoreSlim SEMAPHORE = new(1, 1); + public static class FancyTextGenerator { + private static readonly Dictionary> CharMapByStyle = new(); + private static readonly SemaphoreSlim Semaphore = new(1, 1); private static async Task> GetCharMapAsync(FancyTextStyle style, CancellationToken cancellationToken) { - await SEMAPHORE.WaitAsync(cancellationToken); + await Semaphore.WaitAsync(cancellationToken); try { - if (CHAR_MAP_BY_STYLE.TryGetValue(style, out ImmutableDictionary? charMap)) { + if (CharMapByStyle.TryGetValue(style, out ImmutableDictionary? charMap)) { return charMap; } - using Stream resourceStream = typeof(FancyTextStyle).Assembly.GetManifestResourceStream($"BotNet.Services.FancyText.CharMaps.{style}.json")!; + + await using Stream resourceStream = typeof(FancyTextStyle).Assembly.GetManifestResourceStream($"BotNet.Services.FancyText.CharMaps.{style}.json")!; using StreamReader resourceStreamReader = new(resourceStream); - string resourceText = await resourceStreamReader.ReadToEndAsync(); + string resourceText = await resourceStreamReader.ReadToEndAsync(cancellationToken); Dictionary map = JsonSerializer.Deserialize>(resourceText)!; charMap = map.ToImmutableDictionary(kvp => kvp.Key[0], kvp => kvp.Value); - CHAR_MAP_BY_STYLE.Add(style, charMap!); + CharMapByStyle.Add(style, charMap); return charMap; - } catch { - throw; } finally { - SEMAPHORE.Release(); + Semaphore.Release(); } } diff --git a/BotNet.Services/Gemini/GeminiClient.cs b/BotNet.Services/Gemini/GeminiClient.cs index ad22632..0dd0a77 100644 --- a/BotNet.Services/Gemini/GeminiClient.cs +++ b/BotNet.Services/Gemini/GeminiClient.cs @@ -6,19 +6,15 @@ using System.Threading; using System.Threading.Tasks; using BotNet.Services.Gemini.Models; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace BotNet.Services.Gemini { public class GeminiClient( HttpClient httpClient, - IOptions geminiOptionsAccessor, - ILogger logger + IOptions geminiOptionsAccessor ) { - private const string BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent"; - private readonly HttpClient _httpClient = httpClient; + private const string BaseUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent"; private readonly string _apiKey = geminiOptionsAccessor.Value.ApiKey!; - private readonly ILogger _logger = logger; public async Task ChatAsync(IEnumerable messages, int maxTokens, CancellationToken cancellationToken) { GeminiRequest geminiRequest = new( @@ -33,15 +29,12 @@ public async Task ChatAsync(IEnumerable messages, int maxTokens MaxOutputTokens: maxTokens ) ); - using HttpRequestMessage request = new(HttpMethod.Post, BASE_URL + $"?key={_apiKey}") { - Headers = { - { "Accept", "application/json" } - }, - Content = JsonContent.Create( - inputValue: geminiRequest - ) - }; - using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); + using HttpRequestMessage request = new(HttpMethod.Post, $"{BaseUrl}?key={_apiKey}"); + request.Headers.Add("Accept", "application/json"); + request.Content = JsonContent.Create( + inputValue: geminiRequest + ); + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); string responseContent = await response.Content.ReadAsStringAsync(cancellationToken); response.EnsureSuccessStatusCode(); diff --git a/BotNet.Services/Giphy/GiphyClient.cs b/BotNet.Services/Giphy/GiphyClient.cs index e11706a..4283a58 100644 --- a/BotNet.Services/Giphy/GiphyClient.cs +++ b/BotNet.Services/Giphy/GiphyClient.cs @@ -14,7 +14,7 @@ namespace BotNet.Services.Giphy { [Obsolete("Use TenorClient instead.")] [ExcludeFromCodeCoverage] public class GiphyClient { - private const string GIF_SEARCH_ENDPOINT = "https://api.giphy.com/v1/gifs/search"; + private const string GifSearchEndpoint = "https://api.giphy.com/v1/gifs/search"; private readonly HttpClient _httpClient; private readonly string _apiKey; private readonly JsonSerializerOptions _jsonSerializerOptions; @@ -32,9 +32,9 @@ IOptions giphyOptionsAccessor } public async Task SearchGifsAsync(string query, CancellationToken cancellationToken) { - string requestUrl = $"{GIF_SEARCH_ENDPOINT}?api_key={_apiKey}&q={WebUtility.UrlEncode(query)}&offset=0&limit=25&rating=g&lang=id"; + string requestUrl = $"{GifSearchEndpoint}?api_key={_apiKey}&q={WebUtility.UrlEncode(query)}&offset=0&limit=25&rating=g&lang=id"; GifSearchResult? searchResult = await _httpClient.GetFromJsonAsync(requestUrl, _jsonSerializerOptions, cancellationToken); - return searchResult?.Data ?? Array.Empty(); + return searchResult?.Data ?? []; } } } diff --git a/BotNet.Services/GoogleMap/GeoCode.cs b/BotNet.Services/GoogleMap/GeoCode.cs index 5a7bad0..0487d01 100644 --- a/BotNet.Services/GoogleMap/GeoCode.cs +++ b/BotNet.Services/GoogleMap/GeoCode.cs @@ -12,17 +12,12 @@ namespace BotNet.Services.GoogleMap { /// /// This class intended to get geocoding from address. /// - public class GeoCode { - private readonly string? _apiKey; - private const string URI_TEMPLATE = "https://maps.googleapis.com/maps/api/geocode/json"; - private readonly HttpClientHandler _httpClientHandler; - private readonly HttpClient _httpClient; - - public GeoCode(IOptions options) { - _apiKey = options.Value.ApiKey; - _httpClientHandler = new(); - _httpClient = new(_httpClientHandler); - } + public class GeoCode( + HttpClient httpClient, + IOptions options + ) { + private readonly string? _apiKey = options.Value.ApiKey; + private const string UriTemplate = "https://maps.googleapis.com/maps/api/geocode/json"; /// /// The response of this api call is consist of 2 parts. @@ -43,8 +38,8 @@ public GeoCode(IOptions options) { throw new HttpRequestException("Api key is needed"); } - Uri uri = new(URI_TEMPLATE + $"?address={place}&key={_apiKey}"); - HttpResponseMessage response = await _httpClient.GetAsync(uri.AbsoluteUri); + Uri uri = new($"{UriTemplate}?address={place}&key={_apiKey}"); + HttpResponseMessage response = await httpClient.GetAsync(uri.AbsoluteUri); if (response is not { StatusCode: HttpStatusCode.OK, Content.Headers.ContentType.MediaType: string contentType }) { throw new HttpRequestException("Unable to find location."); @@ -70,7 +65,7 @@ public GeoCode(IOptions options) { throw new HttpRequestException("No Result."); } - Result? result = body.Results[0]; + Result result = body.Results[0]; double lat = result.Geometry!.Location!.Lat; double lng = result.Geometry!.Location!.Lng; diff --git a/BotNet.Services/GoogleMap/Models/Geometry.cs b/BotNet.Services/GoogleMap/Models/Geometry.cs index 1a7335b..0247c10 100644 --- a/BotNet.Services/GoogleMap/Models/Geometry.cs +++ b/BotNet.Services/GoogleMap/Models/Geometry.cs @@ -3,6 +3,7 @@ public class Geometry { public Coordinate? Location{ get; set; } + // ReSharper disable once InconsistentNaming public string? Location_Type { get; set; } public class Coordinate { diff --git a/BotNet.Services/GoogleMap/Models/LocationType.cs b/BotNet.Services/GoogleMap/Models/LocationType.cs index 185303a..654e579 100644 --- a/BotNet.Services/GoogleMap/Models/LocationType.cs +++ b/BotNet.Services/GoogleMap/Models/LocationType.cs @@ -1,4 +1,5 @@ -namespace BotNet.Services.GoogleMap.Models { +// ReSharper disable InconsistentNaming +namespace BotNet.Services.GoogleMap.Models { public enum LocationType { ROOFTOP, RANGE_INTERPOLATED, diff --git a/BotNet.Services/GoogleMap/Models/Result.cs b/BotNet.Services/GoogleMap/Models/Result.cs index d83aaf9..3435197 100644 --- a/BotNet.Services/GoogleMap/Models/Result.cs +++ b/BotNet.Services/GoogleMap/Models/Result.cs @@ -1,6 +1,7 @@ namespace BotNet.Services.GoogleMap.Models { public class Result { + // ReSharper disable once InconsistentNaming public string? Formatted_Address { get; set; } public Geometry? Geometry { get; set; } diff --git a/BotNet.Services/GoogleMap/Models/StatusCode.cs b/BotNet.Services/GoogleMap/Models/StatusCode.cs index 305fcda..6e3b1b8 100644 --- a/BotNet.Services/GoogleMap/Models/StatusCode.cs +++ b/BotNet.Services/GoogleMap/Models/StatusCode.cs @@ -1,4 +1,5 @@ -namespace BotNet.Services.GoogleMap.Models { +// ReSharper disable InconsistentNaming +namespace BotNet.Services.GoogleMap.Models { public enum StatusCode { OK, ZERO_RESULTS, diff --git a/BotNet.Services/GoogleMap/StaticMap.cs b/BotNet.Services/GoogleMap/StaticMap.cs index 588b63c..486b8a0 100644 --- a/BotNet.Services/GoogleMap/StaticMap.cs +++ b/BotNet.Services/GoogleMap/StaticMap.cs @@ -10,11 +10,11 @@ public class StaticMap( IOptions options ) { private readonly string? _apiKey = options.Value.ApiKey; - protected string mapPosition = "center"; - protected int zoom = 13; - protected string size = "600x300"; - protected string marker = "color:red"; - private const string URI_TEMPLATE = "https://maps.googleapis.com/maps/api/staticmap"; + private const string MapPosition = "center"; + private const int Zoom = 13; + private const string Size = "600x300"; + private const string Marker = "color:red"; + private const string UriTemplate = "https://maps.googleapis.com/maps/api/staticmap"; /// /// Get static map image from google map api @@ -30,7 +30,7 @@ public string SearchPlace(string? place) { return "Api key is needed"; } - Uri uri = new(URI_TEMPLATE + $"?{mapPosition}={place}&zoom={zoom}&size={size}&markers={marker}|{place}&key={_apiKey}"); + Uri uri = new($"{UriTemplate}?{MapPosition}={place}&zoom={Zoom}&size={Size}&markers={Marker}|{place}&key={_apiKey}"); return uri.ToString(); } diff --git a/BotNet.Services/GoogleSheets/FromColumnAttribute.cs b/BotNet.Services/GoogleSheets/FromColumnAttribute.cs index 1e943fe..006b0b6 100644 --- a/BotNet.Services/GoogleSheets/FromColumnAttribute.cs +++ b/BotNet.Services/GoogleSheets/FromColumnAttribute.cs @@ -1,12 +1,10 @@ using System; namespace BotNet.Services.GoogleSheets { - [AttributeUsage(validOn: AttributeTargets.Property, AllowMultiple = false)] - public sealed class FromColumnAttribute : Attribute { - public string Column { get; } - - public FromColumnAttribute(string column) { - Column = column; - } + [AttributeUsage(validOn: AttributeTargets.Property)] + public sealed class FromColumnAttribute( + string column + ) : Attribute { + public string Column { get; } = column; } } diff --git a/BotNet.Services/GoogleSheets/GoogleSheetsClient.cs b/BotNet.Services/GoogleSheets/GoogleSheetsClient.cs index c627840..46a0370 100644 --- a/BotNet.Services/GoogleSheets/GoogleSheetsClient.cs +++ b/BotNet.Services/GoogleSheets/GoogleSheetsClient.cs @@ -12,13 +12,11 @@ namespace BotNet.Services.GoogleSheets { public sealed class GoogleSheetsClient( SheetsService sheetsService ) { - private readonly SheetsService _sheetsService = sheetsService; - public async Task> GetDataAsync(string spreadsheetId, string range, string firstColumn, CancellationToken cancellationToken) { int firstColumnIndex = GetColumnIndex(firstColumn); // Fetch data - SpreadsheetsResource.ValuesResource.GetRequest getRequest = _sheetsService.Spreadsheets.Values.Get( + SpreadsheetsResource.ValuesResource.GetRequest getRequest = sheetsService.Spreadsheets.Values.Get( spreadsheetId: spreadsheetId, range: range ); @@ -63,7 +61,7 @@ public async Task> GetDataAsync(string spreadsheetId, string if (decimal.TryParse(value, out decimal decimalValue)) { parameters[i] = decimalValue; } else { - parameters[i] = (decimal?)null; + parameters[i] = null; } } else if (property.PropertyType == typeof(int)) { if (int.TryParse(value, out int intValue)) { @@ -75,7 +73,7 @@ public async Task> GetDataAsync(string spreadsheetId, string if (int.TryParse(value, out int intValue)) { parameters[i] = intValue; } else { - parameters[i] = (int?)null; + parameters[i] = null; } } else if (property.PropertyType == typeof(double)) { if (double.TryParse(value, out double doubleValue)) { @@ -87,7 +85,7 @@ public async Task> GetDataAsync(string spreadsheetId, string if (double.TryParse(value, out double doubleValue)) { parameters[i] = doubleValue; } else { - parameters[i] = (double?)null; + parameters[i] = null; } } else { parameters[i] = Convert.ChangeType(value, property.PropertyType); @@ -100,11 +98,11 @@ public async Task> GetDataAsync(string spreadsheetId, string return builder.ToImmutable(); } - public static int GetColumnIndex(string columnName) { + private static int GetColumnIndex(string columnName) { int index = 0; - for (int i = 0; i < columnName.Length; i++) { + foreach (char c in columnName) { index *= 26; - index += (columnName[i] - 'A' + 1); + index += (c - 'A' + 1); } return index - 1; } diff --git a/BotNet.Services/ImageConverter/IcoToPngConverter.cs b/BotNet.Services/ImageConverter/IcoToPngConverter.cs index 3e7a8a6..21fef00 100644 --- a/BotNet.Services/ImageConverter/IcoToPngConverter.cs +++ b/BotNet.Services/ImageConverter/IcoToPngConverter.cs @@ -5,26 +5,17 @@ using SkiaSharp; namespace BotNet.Services.ImageConverter { - public class IcoToPngConverter { - private readonly HttpClient _httpClient; - - public IcoToPngConverter( - HttpClient httpClient - ) { - _httpClient = httpClient; - } - + public class IcoToPngConverter( + HttpClient httpClient + ) { public async Task ConvertFromUrlAsync(string url, CancellationToken cancellationToken) { - using HttpRequestMessage httpRequest = new(HttpMethod.Get, url) { - Headers = { - { "Accept", "image/ico" }, - { "User-Agent", "TEKNUM" } - } - }; - using HttpResponseMessage response = await _httpClient.SendAsync(httpRequest, cancellationToken); + using HttpRequestMessage httpRequest = new(HttpMethod.Get, url); + httpRequest.Headers.Add("Accept", "image/ico"); + httpRequest.Headers.Add("User-Agent", "TEKNUM"); + using HttpResponseMessage response = await httpClient.SendAsync(httpRequest, cancellationToken); response.EnsureSuccessStatusCode(); - using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken); + await using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken); SKBitmap bitmap = SKBitmap.Decode(stream); SKImage image = SKImage.FromBitmap(bitmap); diff --git a/BotNet.Services/Json/SnakeCaseNamingPolicy.cs b/BotNet.Services/Json/SnakeCaseNamingPolicy.cs index 5166fe6..0bd358c 100644 --- a/BotNet.Services/Json/SnakeCaseNamingPolicy.cs +++ b/BotNet.Services/Json/SnakeCaseNamingPolicy.cs @@ -1,10 +1,24 @@ -using System.Linq; +using System; +using System.Linq; using System.Text.Json; namespace BotNet.Services.Json { public class SnakeCaseNamingPolicy : JsonNamingPolicy { public override string ConvertName(string name) { - return string.Concat(name.Select((c, i) => i > 0 && char.IsUpper(c) ? "_" + c : c.ToString())).ToLower(); + if (name == "") return ""; + Span nameSpan = stackalloc char[name.Length + name.Skip(1).Count(char.IsUpper)]; + int i = 0; + nameSpan[i++] = char.ToLower(name[0]); + foreach (char c in name.Skip(1)) { + if (char.IsUpper(c)) { + nameSpan[i++] = '_'; + nameSpan[i++] = char.ToLower(c); + } else { + nameSpan[i++] = c; + } + } + + return new string(nameSpan); } } } diff --git a/BotNet.Services/Khodam/KhodamCalculator.cs b/BotNet.Services/Khodam/KhodamCalculator.cs index 22c0a4c..46ac5db 100644 --- a/BotNet.Services/Khodam/KhodamCalculator.cs +++ b/BotNet.Services/Khodam/KhodamCalculator.cs @@ -2,7 +2,7 @@ namespace BotNet.Services.Khodam { public static class KhodamCalculator { - private static readonly string[] ANIMALS = [ + private static readonly string[] Animals = [ "Anjing", "Ayam", "Bebek", @@ -26,7 +26,7 @@ public static class KhodamCalculator { "Ular", ]; - private static readonly string[] ADJECTIVES = [ + private static readonly string[] Adjectives = [ "Birahi", "Hitam", "Hutan", @@ -43,7 +43,7 @@ public static class KhodamCalculator { "Sunda", ]; - private static readonly string[] RARES = [ + private static readonly string[] Rares = [ "Ayam Geprek", "Ban Serep", "Bintang Laut", @@ -97,11 +97,11 @@ public static string CalculateKhodam(string name, long userId) { // Rare if (hashCode % 631 > 580) { - return RARES[hashCode % RARES.Length]; + return Rares[hashCode % Rares.Length]; } // Animals - return $"{ANIMALS[hashCode % ANIMALS.Length]} {ADJECTIVES[hashCode % ADJECTIVES.Length]}"; + return $"{Animals[hashCode % Animals.Length]} {Adjectives[hashCode % Adjectives.Length]}"; } } } diff --git a/BotNet.Services/KokizzuVPSBenchmark/ServiceCollectionExtensions.cs b/BotNet.Services/KokizzuVPSBenchmark/ServiceCollectionExtensions.cs index 1fcf397..e85b2f3 100644 --- a/BotNet.Services/KokizzuVPSBenchmark/ServiceCollectionExtensions.cs +++ b/BotNet.Services/KokizzuVPSBenchmark/ServiceCollectionExtensions.cs @@ -3,8 +3,8 @@ namespace BotNet.Services.KokizzuVPSBenchmark { public static class ServiceCollectionExtensions { - public static IServiceCollection AddKokizzuVPSBenchmarkDataSource(this IServiceCollection services) { - services.AddKeyedTransient("vps"); + public static IServiceCollection AddKokizzuVpsBenchmarkDataSource(this IServiceCollection services) { + services.AddKeyedTransient("vps"); return services; } } diff --git a/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmark.cs b/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmark.cs index 00b85cf..aa79792 100644 --- a/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmark.cs +++ b/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmark.cs @@ -1,7 +1,7 @@ using BotNet.Services.GoogleSheets; namespace BotNet.Services.KokizzuVPSBenchmark { - public sealed record VPSBenchmark( + public sealed record VpsBenchmark( [property: FromColumn("A")] string Provider, [property: FromColumn("B")] string Location, [property: FromColumn("C")] string BenchmarkDate, diff --git a/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmarkDataSource.cs b/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmarkDataSource.cs index 820ff4b..19b8b75 100644 --- a/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmarkDataSource.cs +++ b/BotNet.Services/KokizzuVPSBenchmark/VPSBenchmarkDataSource.cs @@ -6,15 +6,12 @@ using BotNet.Services.Sqlite; namespace BotNet.Services.KokizzuVPSBenchmark { - public sealed class VPSBenchmarkDataSource( + public sealed class VpsBenchmarkDataSource( GoogleSheetsClient googleSheetsClient, ScopedDatabase scopedDatabase ) : IScopedDataSource { - private readonly GoogleSheetsClient _googleSheetsClient = googleSheetsClient; - private readonly ScopedDatabase _scopedDatabase = scopedDatabase; - public async Task LoadTableAsync(CancellationToken cancellationToken) { - _scopedDatabase.ExecuteNonQuery(""" + scopedDatabase.ExecuteNonQuery(""" CREATE TABLE vps ( Provider TEXT, Location VARCHAR(2), @@ -37,7 +34,7 @@ AvgMbs REAL ) """); - ImmutableList data = await _googleSheetsClient.GetDataAsync( + ImmutableList data = await googleSheetsClient.GetDataAsync( // Source: https://docs.google.com/spreadsheets/d/14nAIFzIzkQuSxiayhc5tSFWFCWFncrV-GCA3Q5BbS4g/edit#gid=0 spreadsheetId: "14nAIFzIzkQuSxiayhc5tSFWFCWFncrV-GCA3Q5BbS4g", range: "'Result'!A3:T", @@ -45,8 +42,8 @@ AvgMbs REAL cancellationToken: cancellationToken ); - foreach (VPSBenchmark vpsBenchmark in data) { - _scopedDatabase.ExecuteNonQuery($""" + foreach (VpsBenchmark vpsBenchmark in data) { + scopedDatabase.ExecuteNonQuery($""" INSERT INTO vps ( Provider, Location, diff --git a/BotNet.Services/MarkdownV2/MarkdownV2Sanitizer.cs b/BotNet.Services/MarkdownV2/MarkdownV2Sanitizer.cs index 776c00e..9104f3c 100644 --- a/BotNet.Services/MarkdownV2/MarkdownV2Sanitizer.cs +++ b/BotNet.Services/MarkdownV2/MarkdownV2Sanitizer.cs @@ -3,7 +3,7 @@ namespace BotNet.Services.MarkdownV2 { public static class MarkdownV2Sanitizer { - private static readonly HashSet CHARACTERS_TO_ESCAPE = [ + private static readonly HashSet CharactersToEscape = [ '[', ']', '(', ')', '~', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!' ]; @@ -18,7 +18,7 @@ public static string Sanitize(string input) { char previousCharacter = '\0'; foreach (char character in input) { // If the character is in our list, append a backslash before it - if (CHARACTERS_TO_ESCAPE.Contains(character) + if (CharactersToEscape.Contains(character) && previousCharacter != '\\') { sanitized.Append('\\'); } @@ -28,17 +28,5 @@ public static string Sanitize(string input) { return sanitized.ToString(); } - - private static int CountOccurences(string text, string substring) { - int count = 0; - int i = 0; - - while ((i = text.IndexOf(substring, i)) != -1) { - i += substring.Length; - count++; - } - - return count; - } } } diff --git a/BotNet.Services/Meme/MemeGenerator.cs b/BotNet.Services/Meme/MemeGenerator.cs index 33d3266..d5faa9b 100644 --- a/BotNet.Services/Meme/MemeGenerator.cs +++ b/BotNet.Services/Meme/MemeGenerator.cs @@ -5,17 +5,11 @@ using SkiaSharp; namespace BotNet.Services.Meme { - public class MemeGenerator { - private readonly BotNetFontService _botNetFontService; - - public MemeGenerator( - BotNetFontService botNetFontService - ) { - _botNetFontService = botNetFontService; - } - + public class MemeGenerator( + BotNetFontService botNetFontService + ) { public byte[] CaptionRamad(string text) { - return CaptionMeme(Templates.RAMAD, text); + return CaptionMeme(Templates.Ramad, text); } private byte[] CaptionMeme(Template template, string text) { @@ -33,15 +27,14 @@ private byte[] CaptionMeme(Template template, string text) { canvas.Save(); canvas.RotateDegrees(template.Rotation); - using Stream fontStream = _botNetFontService.GetFontStyleById(template.FontStyleId).OpenStream(); + using Stream fontStream = botNetFontService.GetFontStyleById(template.FontStyleId).OpenStream(); using SKTypeface typeface = SKTypeface.FromStream(fontStream); - using SKPaint paint = new() { - TextAlign = template.TextAlign, - Color = template.TextColor, - Typeface = typeface, - TextSize = template.TextSize, - IsAntialias = true - }; + using SKPaint paint = new(); + paint.TextAlign = template.TextAlign; + paint.Color = template.TextColor; + paint.Typeface = typeface; + paint.TextSize = template.TextSize; + paint.IsAntialias = true; float offset = 0f; foreach (string line in WrapWords(text, template.MaxWidth, paint)) { canvas.DrawText( @@ -65,7 +58,7 @@ private byte[] CaptionMeme(Template template, string text) { private static List WrapWords(string text, float maxWidth, SKPaint paint) { string[] words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - List lines = new(); + List lines = []; bool firstWord = true; string line = ""; foreach (string word in words) { diff --git a/BotNet.Services/Meme/Templates.cs b/BotNet.Services/Meme/Templates.cs index 4b5d1b5..69093f5 100644 --- a/BotNet.Services/Meme/Templates.cs +++ b/BotNet.Services/Meme/Templates.cs @@ -15,7 +15,7 @@ float Y ); internal static class Templates { - public static readonly Template RAMAD = new( + public static readonly Template Ramad = new( ImageResourceName: "BotNet.Services.Meme.Images.Ramad.jpg", FontStyleId: "Inter-Regular", TextAlign: SKTextAlign.Left, diff --git a/BotNet.Services/OpenAI/AttachmentGenerator.cs b/BotNet.Services/OpenAI/AttachmentGenerator.cs index 5b02cf9..246cbba 100644 --- a/BotNet.Services/OpenAI/AttachmentGenerator.cs +++ b/BotNet.Services/OpenAI/AttachmentGenerator.cs @@ -9,14 +9,14 @@ namespace BotNet.Services.OpenAI { public partial class AttachmentGenerator( IOptions hostingOptionsAccessor ) { - private static readonly Regex HEX_COLOR_CODE_PATTERN = HexColorCodeRegex(); + private static readonly Regex HexColorCodePattern = HexColorCodeRegex(); private readonly HostingOptions _hostingOptions = hostingOptionsAccessor.Value; public ImmutableList GenerateAttachments(string message) { ImmutableList.Builder builder = ImmutableList.Empty.ToBuilder(); // Detect hex color codes - MatchCollection matches = HEX_COLOR_CODE_PATTERN.Matches(message); + MatchCollection matches = HexColorCodePattern.Matches(message); foreach (Match match in matches) { builder.Add(new Uri($"https://{_hostingOptions.HostName}/renderer/color?name={WebUtility.UrlEncode(match.Value)}")); } diff --git a/BotNet.Services/OpenAI/IntentDetector.cs b/BotNet.Services/OpenAI/IntentDetector.cs index d4c8d15..5bbff39 100644 --- a/BotNet.Services/OpenAI/IntentDetector.cs +++ b/BotNet.Services/OpenAI/IntentDetector.cs @@ -5,10 +5,8 @@ namespace BotNet.Services.OpenAI { public sealed class IntentDetector( - OpenAIClient openAIClient + OpenAiClient openAiClient ) { - private readonly OpenAIClient _openAIClient = openAIClient; - public async Task DetectChatIntentAsync( string message, CancellationToken cancellationToken @@ -29,7 +27,7 @@ CancellationToken cancellationToken """) ]; - string answer = await _openAIClient.ChatAsync( + string answer = await openAiClient.ChatAsync( model: "gpt-3.5-turbo", messages: messages, maxTokens: 128, @@ -64,7 +62,7 @@ CancellationToken cancellationToken """) ]; - string answer = await _openAIClient.ChatAsync( + string answer = await openAiClient.ChatAsync( model: "gpt-3.5-turbo", messages: messages, maxTokens: 128, diff --git a/BotNet.Services/OpenAI/OpenAIClient.cs b/BotNet.Services/OpenAI/OpenAIClient.cs index e5b5954..684dde6 100644 --- a/BotNet.Services/OpenAI/OpenAIClient.cs +++ b/BotNet.Services/OpenAI/OpenAIClient.cs @@ -15,46 +15,42 @@ using RG.Ninja; namespace BotNet.Services.OpenAI { - public class OpenAIClient( + public class OpenAiClient( HttpClient httpClient, - IOptions openAIOptionsAccessor, - ILogger logger + IOptions openAiOptionsAccessor, + ILogger logger ) { - private const string COMPLETION_URL_TEMPLATE = "https://api.openai.com/v1/engines/{0}/completions"; - private const string CHAT_URL = "https://api.openai.com/v1/chat/completions"; - private const string IMAGE_GENERATION_URL = "https://api.openai.com/v1/images/generations"; - private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { + private const string CompletionUrlTemplate = "https://api.openai.com/v1/engines/{0}/completions"; + private const string ChatUrl = "https://api.openai.com/v1/chat/completions"; + private const string ImageGenerationUrl = "https://api.openai.com/v1/images/generations"; + private static readonly JsonSerializerOptions JsonSerializerOptions = new() { PropertyNamingPolicy = new SnakeCaseNamingPolicy() }; - private readonly HttpClient _httpClient = httpClient; - private readonly string _apiKey = openAIOptionsAccessor.Value.ApiKey!; - private readonly ILogger _logger = logger; + + private readonly string _apiKey = openAiOptionsAccessor.Value.ApiKey!; public async Task AutocompleteAsync(string engine, string prompt, string[]? stop, int maxTokens, double frequencyPenalty, double presencePenalty, double temperature, double topP, CancellationToken cancellationToken) { - using HttpRequestMessage request = new(HttpMethod.Post, string.Format(COMPLETION_URL_TEMPLATE, engine)) { - Headers = { - { "Authorization", $"Bearer {_apiKey}" }, - { "Accept", "text/event-stream" } + using HttpRequestMessage request = new(HttpMethod.Post, string.Format(CompletionUrlTemplate, engine)); + request.Headers.Add("Authorization", $"Bearer {_apiKey}"); + request.Headers.Add("Accept", "text/event-stream"); + request.Content = JsonContent.Create( + inputValue: new { + Prompt = prompt, + Temperature = temperature, + MaxTokens = maxTokens, + Stream = true, + TopP = topP, + FrequencyPenalty = frequencyPenalty, + PresencePenalty = presencePenalty, + Stop = stop }, - Content = JsonContent.Create( - inputValue: new { - Prompt = prompt, - Temperature = temperature, - MaxTokens = maxTokens, - Stream = true, - TopP = topP, - FrequencyPenalty = frequencyPenalty, - PresencePenalty = presencePenalty, - Stop = stop - }, - options: JSON_SERIALIZER_OPTIONS - ) - }; - using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); + options: JsonSerializerOptions + ); + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); StringBuilder result = new(); - using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken); + await using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken); using StreamReader streamReader = new(stream); while (!streamReader.EndOfStream) { string? line = await streamReader.ReadLineAsync(cancellationToken); @@ -62,7 +58,7 @@ public async Task AutocompleteAsync(string engine, string prompt, string if (line == "") continue; if (!line.StartsWith("data: ", out string? json)) break; if (json == "[DONE]") break; - CompletionResult? completionResult = JsonSerializer.Deserialize(json, JSON_SERIALIZER_OPTIONS); + CompletionResult? completionResult = JsonSerializer.Deserialize(json, JsonSerializerOptions); if (completionResult == null) break; if (completionResult.Choices.Count == 0) break; result.Append(completionResult.Choices[0].Text); @@ -73,23 +69,20 @@ public async Task AutocompleteAsync(string engine, string prompt, string } public async Task ChatAsync(string model, IEnumerable messages, int maxTokens, CancellationToken cancellationToken) { - using HttpRequestMessage request = new(HttpMethod.Post, CHAT_URL) { - Headers = { - { "Authorization", $"Bearer {_apiKey}" }, + using HttpRequestMessage request = new(HttpMethod.Post, ChatUrl); + request.Headers.Add("Authorization", $"Bearer {_apiKey}"); + request.Content = JsonContent.Create( + inputValue: new { + Model = model, + MaxTokens = maxTokens, + Messages = messages }, - Content = JsonContent.Create( - inputValue: new { - Model = model, - MaxTokens = maxTokens, - Messages = messages - }, - options: JSON_SERIALIZER_OPTIONS - ) - }; - using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); + options: JsonSerializerOptions + ); + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); - CompletionResult? completionResult = await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, cancellationToken); + CompletionResult? completionResult = await response.Content.ReadFromJsonAsync(JsonSerializerOptions, cancellationToken); if (completionResult == null) return ""; if (completionResult.Choices.Count == 0) return ""; return completionResult.Choices[0].Message?.Content!; @@ -101,30 +94,27 @@ public async Task ChatAsync(string model, IEnumerable messa int maxTokens, [EnumeratorCancellation] CancellationToken cancellationToken ) { - using HttpRequestMessage request = new(HttpMethod.Post, CHAT_URL) { - Headers = { - { "Authorization", $"Bearer {_apiKey}" }, - { "Accept", "text/event-stream" } + using HttpRequestMessage request = new(HttpMethod.Post, ChatUrl); + request.Headers.Add("Authorization", $"Bearer {_apiKey}"); + request.Headers.Add("Accept", "text/event-stream"); + request.Content = JsonContent.Create( + inputValue: new { + Model = model, + MaxTokens = maxTokens, + Messages = messages, + Stream = true }, - Content = JsonContent.Create( - inputValue: new { - Model = model, - MaxTokens = maxTokens, - Messages = messages, - Stream = true - }, - options: JSON_SERIALIZER_OPTIONS - ) - }; - using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); + options: JsonSerializerOptions + ); + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { string errorMessage = await response.Content.ReadAsStringAsync(cancellationToken); - _logger.LogError(errorMessage); + logger.LogError(errorMessage); response.EnsureSuccessStatusCode(); } StringBuilder result = new(); - using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken); + await using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken); using StreamReader streamReader = new(stream); while (!streamReader.EndOfStream) { @@ -149,7 +139,7 @@ [EnumeratorCancellation] CancellationToken cancellationToken yield break; } - CompletionResult? completionResult = JsonSerializer.Deserialize(json, JSON_SERIALIZER_OPTIONS); + CompletionResult? completionResult = JsonSerializer.Deserialize(json, JsonSerializerOptions); if (completionResult == null || completionResult.Choices.Count == 0) { yield return ( @@ -167,34 +157,31 @@ [EnumeratorCancellation] CancellationToken cancellationToken Stop: true ); yield break; - } else { - yield return ( - Result: result.ToString(), - Stop: false - ); } + + yield return ( + Result: result.ToString(), + Stop: false + ); } } public async Task GenerateImageAsync(string model, string prompt, CancellationToken cancellationToken) { - using HttpRequestMessage request = new(HttpMethod.Post, IMAGE_GENERATION_URL) { - Headers = { - { "Authorization", $"Bearer {_apiKey}" } + using HttpRequestMessage request = new(HttpMethod.Post, ImageGenerationUrl); + request.Headers.Add("Authorization", $"Bearer {_apiKey}"); + request.Content = JsonContent.Create( + inputValue: new { + Model = model, + Prompt = prompt, + N = 1, + Size = "1024x1024" }, - Content = JsonContent.Create( - inputValue: new { - Model = model, - Prompt = prompt, - N = 1, - Size = "1024x1024" - }, - options: JSON_SERIALIZER_OPTIONS - ) - }; - using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); + options: JsonSerializerOptions + ); + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); - ImageGenerationResult? imageGenerationResult = await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, cancellationToken); + ImageGenerationResult? imageGenerationResult = await response.Content.ReadFromJsonAsync(JsonSerializerOptions, cancellationToken); return new(imageGenerationResult!.Data[0].Url); } } diff --git a/BotNet.Services/OpenAI/OpenAIOptions.cs b/BotNet.Services/OpenAI/OpenAIOptions.cs index 131960e..ccd0ba9 100644 --- a/BotNet.Services/OpenAI/OpenAIOptions.cs +++ b/BotNet.Services/OpenAI/OpenAIOptions.cs @@ -1,5 +1,5 @@ namespace BotNet.Services.OpenAI { - public class OpenAIOptions { - public string? ApiKey { get; set; } + public class OpenAiOptions { + public string? ApiKey { get; init; } } } diff --git a/BotNet.Services/OpenAI/OpenAIStreamingClient.cs b/BotNet.Services/OpenAI/OpenAIStreamingClient.cs index 795c4dc..46ee13d 100644 --- a/BotNet.Services/OpenAI/OpenAIStreamingClient.cs +++ b/BotNet.Services/OpenAI/OpenAIStreamingClient.cs @@ -11,12 +11,10 @@ using Telegram.Bot.Types.Enums; namespace BotNet.Services.OpenAI { - public sealed class OpenAIStreamingClient( + public sealed class OpenAiStreamingClient( IServiceProvider serviceProvider, - ILogger logger + ILogger logger ) : IDisposable { - private readonly IServiceProvider _serviceProvider = serviceProvider; - private readonly ILogger _logger = logger; private IServiceScope? _danglingServiceScope; private bool _disposedValue; @@ -28,12 +26,12 @@ public async Task StreamChatAsync( long chatId, int replyToMessageId ) { - IServiceScope serviceScope = _serviceProvider.CreateScope(); + IServiceScope serviceScope = serviceProvider.CreateScope(); _danglingServiceScope = serviceScope; - OpenAIClient openAIClient = serviceScope.ServiceProvider.GetRequiredService(); + OpenAiClient openAiClient = serviceScope.ServiceProvider.GetRequiredService(); ITelegramBotClient telegramBotClient = serviceScope.ServiceProvider.GetRequiredService(); - IAsyncEnumerable<(string Result, bool Stop)> enumerable = openAIClient.StreamChatAsync( + IAsyncEnumerable<(string Result, bool Stop)> enumerable = openAiClient.StreamChatAsync( model: model, messages: messages, maxTokens: maxTokens, @@ -53,7 +51,7 @@ int replyToMessageId } } } catch (Exception exc) { - _logger.LogError(exc, null); + logger.LogError(exc, null); } }); @@ -108,6 +106,7 @@ await Task.WhenAny( _ = Task.Run(async () => { try { while (!downstreamTask.IsCompleted) { + // ReSharper disable once AccessToDisposedClosure await Task.Delay(TimeSpan.FromSeconds(3), cts.Token); if (lastSent != lastResult) { @@ -118,11 +117,12 @@ await telegramBotClient.EditMessageText( messageId: incompleteMessage.MessageId, text: MarkdownV2Sanitizer.Sanitize(lastResult ?? "") + "… ⏳", // ellipsis, nbsp, hourglass emoji parseMode: ParseMode.MarkdownV2, + // ReSharper disable once AccessToDisposedClosure cancellationToken: cts.Token ); } catch (Exception exc) when (exc is not OperationCanceledException) { // Message might be deleted - _logger.LogError(exc, null); + logger.LogError(exc, null); break; } } @@ -145,7 +145,7 @@ await telegramBotClient.EditMessageText( cancellationToken: cts.Token ); } catch (Exception exc) { - _logger.LogError(exc, null); + logger.LogError(exc, null); throw; } @@ -162,7 +162,7 @@ await telegramBotClient.EditMessageText( // Message might be deleted, suppress exception } - cts.Cancel(); + await cts.CancelAsync(); serviceScope.Dispose(); }); } diff --git a/BotNet.Services/OpenAI/ServiceCollectionExtensions.cs b/BotNet.Services/OpenAI/ServiceCollectionExtensions.cs index 1119442..a064012 100644 --- a/BotNet.Services/OpenAI/ServiceCollectionExtensions.cs +++ b/BotNet.Services/OpenAI/ServiceCollectionExtensions.cs @@ -3,9 +3,9 @@ namespace BotNet.Services.OpenAI { public static class ServiceCollectionExtensions { - public static IServiceCollection AddOpenAIClient(this IServiceCollection services) { - services.AddTransient(); - services.AddTransient(); + public static IServiceCollection AddOpenAiClient(this IServiceCollection services) { + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/BotNet.Services/OpenAI/Skills/AssistantBot.cs b/BotNet.Services/OpenAI/Skills/AssistantBot.cs index 28fb4f2..bfdfd37 100644 --- a/BotNet.Services/OpenAI/Skills/AssistantBot.cs +++ b/BotNet.Services/OpenAI/Skills/AssistantBot.cs @@ -3,29 +3,30 @@ namespace BotNet.Services.OpenAI.Skills { public class AssistantBot( - OpenAIClient openAIClient + OpenAiClient openAiClient ) { - private readonly OpenAIClient _openAIClient = openAIClient; - - public Task AskSomethingAsync(string name, string question, CancellationToken cancellationToken) { + public Task AskSomethingAsync( + string question, + CancellationToken cancellationToken + ) { string prompt = $"I am a highly intelligent question answering bot. If you ask me a question that is rooted in truth, I will give you the answer. If you ask me a question that is nonsense, trickery, or has no clear answer, I will respond with \"Unknown\".\n\n" - + "Q: What is human life expectancy in the United States?\n" - + "A: Human life expectancy in the United States is 78 years.\n\n" - + "Q: Who was president of the United States in 1955?\n" - + "A: Dwight D. Eisenhower was president of the United States in 1955.\n\n" - + "Q: Which party did he belong to?\n" - + "A: He belonged to the Republican Party.\n\n" - + "Q: What is the square root of banana?\n" - + "A: Unknown\n\n" - + "Q: How does a telescope work?\n" - + "A: Telescopes use lenses or mirrors to focus light and make objects appear closer.\n\n" - + "Q: Where were the 1992 Olympics held?\n" - + "A: The 1992 Olympics were held in Barcelona, Spain.\n\n" - + "Q: How many squigs are in a bonk?\n" - + "A: Unknown\n\n" - + $"Q: {question}\n" - + "A:"; - return _openAIClient.AutocompleteAsync( + + "Q: What is human life expectancy in the United States?\n" + + "A: Human life expectancy in the United States is 78 years.\n\n" + + "Q: Who was president of the United States in 1955?\n" + + "A: Dwight D. Eisenhower was president of the United States in 1955.\n\n" + + "Q: Which party did he belong to?\n" + + "A: He belonged to the Republican Party.\n\n" + + "Q: What is the square root of banana?\n" + + "A: Unknown\n\n" + + "Q: How does a telescope work?\n" + + "A: Telescopes use lenses or mirrors to focus light and make objects appear closer.\n\n" + + "Q: Where were the 1992 Olympics held?\n" + + "A: The 1992 Olympics were held in Barcelona, Spain.\n\n" + + "Q: How many squigs are in a bonk?\n" + + "A: Unknown\n\n" + + $"Q: {question}\n" + + "A:"; + return openAiClient.AutocompleteAsync( engine: "text-davinci-002", prompt: prompt, stop: ["\n"], diff --git a/BotNet.Services/OpenAI/Skills/FriendlyBot.cs b/BotNet.Services/OpenAI/Skills/FriendlyBot.cs index b1a4013..6a551b4 100644 --- a/BotNet.Services/OpenAI/Skills/FriendlyBot.cs +++ b/BotNet.Services/OpenAI/Skills/FriendlyBot.cs @@ -7,19 +7,21 @@ namespace BotNet.Services.OpenAI.Skills { public sealed class FriendlyBot( - OpenAIClient openAIClient, - OpenAIStreamingClient openAIStreamingClient + OpenAiClient openAiClient, + OpenAiStreamingClient openAiStreamingClient ) { - private readonly OpenAIClient _openAIClient = openAIClient; - private readonly OpenAIStreamingClient _openAIStreamingClient = openAIStreamingClient; - - public Task ChatAsync(string callSign, string name, string question, CancellationToken cancellationToken) { + public Task ChatAsync( + string callSign, + string name, + string question, + CancellationToken cancellationToken + ) { string prompt = $"The following is a conversation with an AI assistant. The assistant is helpful, creative, direct, concise, and always get to the point.\n\n" - + $"{name}: Hello, how are you?\n" - + $"{callSign}: I am an AI created by TEKNUM. How can I help you today?\n\n" - + $"{name}: {question}\n" - + $"{callSign}: "; - return _openAIClient.AutocompleteAsync( + + $"{name}: Hello, how are you?\n" + + $"{callSign}: I am an AI created by TEKNUM. How can I help you today?\n\n" + + $"{name}: {question}\n" + + $"{callSign}: "; + return openAiClient.AutocompleteAsync( engine: "text-davinci-003", prompt: prompt, stop: [$"{name}:"], @@ -32,18 +34,25 @@ public Task ChatAsync(string callSign, string name, string question, Can ); } - public Task RespondToThreadAsync(string callSign, string name, string question, ImmutableList<(string Sender, string Text)> thread, CancellationToken cancellationToken) { + public Task RespondToThreadAsync( + string callSign, + string name, + string question, + ImmutableList<(string Sender, string Text)> thread, + CancellationToken cancellationToken + ) { string prompt = $"The following is a conversation with an AI assistant. The assistant is helpful, creative, direct, concise, and always get to the point.\n\n" - + $"{name}: Hello, how are you?\n" - + $"{callSign}: I am an AI created by TEKNUM. How can I help you today?\n\n"; + + $"{name}: Hello, how are you?\n" + + $"{callSign}: I am an AI created by TEKNUM. How can I help you today?\n\n"; foreach ((string sender, string text) in thread) { prompt += $"{sender}: {text}\n"; if (sender is "GPT" or "Pakde") prompt += "\n"; } + prompt += $"{name}: {question}\n" + $"{callSign}: "; - return _openAIClient.AutocompleteAsync( + return openAiClient.AutocompleteAsync( engine: "text-davinci-003", prompt: prompt, stop: [$"{name}:"], @@ -56,13 +65,16 @@ public Task RespondToThreadAsync(string callSign, string name, string qu ); } - public Task ChatAsync(string message, CancellationToken cancellationToken) { + public Task ChatAsync( + string message, + CancellationToken cancellationToken + ) { List messages = [ ChatMessage.FromText("system", "The following is a conversation with an AI assistant. The assistant is helpful, creative, direct, concise, and always get to the point."), ChatMessage.FromText("user", message) ]; - return _openAIClient.ChatAsync( + return openAiClient.ChatAsync( model: "gpt-4-1106-preview", messages: messages, maxTokens: 512, @@ -70,13 +82,17 @@ public Task ChatAsync(string message, CancellationToken cancellationToke ); } - public async Task StreamChatAsync(string message, long chatId, int replyToMessageId) { + public async Task StreamChatAsync( + string message, + long chatId, + int replyToMessageId + ) { List messages = [ ChatMessage.FromText("system", "The following is a conversation with an AI assistant. The assistant is helpful, creative, direct, concise, and always get to the point."), ChatMessage.FromText("user", message) ]; - await _openAIStreamingClient.StreamChatAsync( + await openAiStreamingClient.StreamChatAsync( model: "gpt-4-1106-preview", messages: messages, maxTokens: 512, @@ -86,10 +102,13 @@ await _openAIStreamingClient.StreamChatAsync( ); } - public Task ChatAsync(string message, ImmutableList<(string Sender, string? Text, string? ImageBase64)> thread, CancellationToken cancellationToken) { + public Task ChatAsync( + string message, + ImmutableList<(string Sender, string? Text, string? ImageBase64)> thread, + CancellationToken cancellationToken + ) { List messages = new() { ChatMessage.FromText("system", "The following is a conversation with an AI assistant. The assistant is helpful, creative, direct, concise, and always get to the point."), - from tuple in thread let role = tuple.Sender switch { "GPT" => "assistant", @@ -101,11 +120,10 @@ from tuple in thread { Text: { } text, ImageBase64: { } imageBase64 } => ChatMessage.FromTextWithImageBase64(role, text, imageBase64), _ => ChatMessage.FromText(role, "") }, - ChatMessage.FromText("user", message) }; - return _openAIClient.ChatAsync( + return openAiClient.ChatAsync( model: "gpt-4-1106-preview", messages: messages, maxTokens: 512, @@ -113,10 +131,14 @@ from tuple in thread ); } - public async Task StreamChatAsync(string message, ImmutableList<(string Sender, string? Text, string? ImageBase64)> thread, long chatId, int replyToMessageId) { + public async Task StreamChatAsync( + string message, + ImmutableList<(string Sender, string? Text, string? ImageBase64)> thread, + long chatId, + int replyToMessageId + ) { List messages = new() { ChatMessage.FromText("system", "The following is a conversation with an AI assistant. The assistant is helpful, creative, direct, concise, and always get to the point."), - from tuple in thread let role = tuple.Sender switch { "GPT" => "assistant", @@ -128,11 +150,10 @@ from tuple in thread { Text: { } text, ImageBase64: { } imageBase64 } => ChatMessage.FromTextWithImageBase64(role, text, imageBase64), _ => ChatMessage.FromText(role, "") }, - ChatMessage.FromText("user", message) }; - await _openAIStreamingClient.StreamChatAsync( + await openAiStreamingClient.StreamChatAsync( model: "gpt-4-1106-preview", messages: messages, maxTokens: 512, diff --git a/BotNet.Services/OpenAI/Skills/ImageGenerationBot.cs b/BotNet.Services/OpenAI/Skills/ImageGenerationBot.cs index b5f1e08..a83c50b 100644 --- a/BotNet.Services/OpenAI/Skills/ImageGenerationBot.cs +++ b/BotNet.Services/OpenAI/Skills/ImageGenerationBot.cs @@ -4,25 +4,24 @@ namespace BotNet.Services.OpenAI.Skills { public class ImageGenerationBot( - OpenAIClient openAIClient + OpenAiClient openAiClient ) { - private readonly OpenAIClient _openAIClient = openAIClient; - private static readonly SemaphoreSlim SEMAPHORE = new(1, 1); + private static readonly SemaphoreSlim Semaphore = new(1, 1); public async Task GenerateImageAsync( string prompt, CancellationToken cancellationToken ) { // dall-e-3 endpoint does not allow concurrent requests - await SEMAPHORE.WaitAsync(cancellationToken); + await Semaphore.WaitAsync(cancellationToken); try { - return await _openAIClient.GenerateImageAsync( + return await openAiClient.GenerateImageAsync( model: "dall-e-3", prompt: prompt, cancellationToken: cancellationToken ); } finally { - SEMAPHORE.Release(); + Semaphore.Release(); } } } diff --git a/BotNet.Services/OpenAI/Skills/SarcasticBot.cs b/BotNet.Services/OpenAI/Skills/SarcasticBot.cs index 5d45139..3dfd76a 100644 --- a/BotNet.Services/OpenAI/Skills/SarcasticBot.cs +++ b/BotNet.Services/OpenAI/Skills/SarcasticBot.cs @@ -4,10 +4,8 @@ namespace BotNet.Services.OpenAI.Skills { public class SarcasticBot( - OpenAIClient openAIClient + OpenAiClient openAiClient ) { - private readonly OpenAIClient _openAIClient = openAIClient; - public Task ChatAsync(string callSign, string name, string question, CancellationToken cancellationToken) { string prompt = $"{callSign} adalah chatbot berbahasa Indonesia yang tidak ramah, kurang antusias dalam menjawab pertanyaan, dan suka mengomel.\n\n" + $"{name}: Satu kilogram itu berapa pound?\n" @@ -20,7 +18,7 @@ public Task ChatAsync(string callSign, string name, string question, Can + $"{callSign}: Entahlah. Nanti coba saya tanya ke teman saya Google.\n\n" + $"{name}: {question}\n" + $"{callSign}: "; - return _openAIClient.AutocompleteAsync( + return openAiClient.AutocompleteAsync( engine: "text-curie-001", prompt: prompt, stop: [$"{name}:"], @@ -43,14 +41,14 @@ public Task RespondToThreadAsync(string callSign, string name, string qu + $"{callSign}: Tanggal 17 Desember 1903, Wilbur dan Orville Wright menerbangkan pesawat terbang pertama dalam sejarah. Semoga mereka mengangkut saya dari sini.\n\n" + $"{name}: Apa makna kehidupan?\n" + $"{callSign}: Entahlah. Nanti coba saya tanya ke teman saya Google.\n\n"; - foreach ((string sender, string? text, string? imageBase64) in thread) { + foreach ((string sender, string? text, string? _) in thread) { prompt += $"{sender}: {text}\n"; if (sender is "GPT" or "Pakde") prompt += "\n"; } prompt += $"{name}: {question}\n" + $"{callSign}: "; - return _openAIClient.AutocompleteAsync( + return openAiClient.AutocompleteAsync( engine: "text-curie-001", prompt: prompt, stop: [$"{name}:"], diff --git a/BotNet.Services/OpenAI/Skills/TldrGenerator.cs b/BotNet.Services/OpenAI/Skills/TldrGenerator.cs index 4d4db05..902644a 100644 --- a/BotNet.Services/OpenAI/Skills/TldrGenerator.cs +++ b/BotNet.Services/OpenAI/Skills/TldrGenerator.cs @@ -3,13 +3,11 @@ namespace BotNet.Services.OpenAI.Skills { public class TldrGenerator( - OpenAIClient openAIClient + OpenAiClient openAiClient ) { - private readonly OpenAIClient _openAIClient = openAIClient; - public Task GenerateTldrAsync(string text, CancellationToken cancellationToken) { string prompt = $"{text}\n\nTl;dr:\n"; - return _openAIClient.AutocompleteAsync( + return openAiClient.AutocompleteAsync( engine: "text-davinci-002", prompt: prompt, stop: null, diff --git a/BotNet.Services/OpenAI/Skills/Translator.cs b/BotNet.Services/OpenAI/Skills/Translator.cs index b7d14cc..d8f68e7 100644 --- a/BotNet.Services/OpenAI/Skills/Translator.cs +++ b/BotNet.Services/OpenAI/Skills/Translator.cs @@ -4,10 +4,8 @@ namespace BotNet.Services.OpenAI.Skills { public class Translator( - OpenAIClient openAIClient + OpenAiClient openAiClient ) { - private readonly OpenAIClient _openAIClient = openAIClient; - public async Task TranslateAsync(string sentence, string languagePair, CancellationToken cancellationToken) { switch (languagePair) { case "eniden": @@ -23,7 +21,7 @@ public async Task TranslateAsync(string sentence, string languagePair, C cancellationToken: cancellationToken ); } - string? prompt = languagePair switch { + string prompt = languagePair switch { "enid" => "English: I do not speak Indonesian.\n" + "Indonesian: Saya tidak bisa berbicara bahasa Indonesia.\n\n" @@ -54,7 +52,7 @@ public async Task TranslateAsync(string sentence, string languagePair, C + "English:", _ => throw new NotImplementedException() }; - return await _openAIClient.AutocompleteAsync( + return await openAiClient.AutocompleteAsync( engine: "text-davinci-002", prompt: prompt, stop: ["\n"], diff --git a/BotNet.Services/OpenAI/Skills/VisionBot.cs b/BotNet.Services/OpenAI/Skills/VisionBot.cs index 05d8db5..42482df 100644 --- a/BotNet.Services/OpenAI/Skills/VisionBot.cs +++ b/BotNet.Services/OpenAI/Skills/VisionBot.cs @@ -6,10 +6,8 @@ namespace BotNet.Services.OpenAI.Skills { public sealed class VisionBot( - OpenAIStreamingClient openAIStreamingClient + OpenAiStreamingClient openAiStreamingClient ) { - private readonly OpenAIStreamingClient _openAIStreamingClient = openAIStreamingClient; - public async Task StreamChatAsync( string message, string imageBase64, @@ -21,7 +19,7 @@ int replyToMessageId ChatMessage.FromTextWithImageBase64("user", message, imageBase64) }; - await _openAIStreamingClient.StreamChatAsync( + await openAiStreamingClient.StreamChatAsync( model: "gpt-4-vision-preview", messages: messages, maxTokens: 512, @@ -56,7 +54,7 @@ from tuple in thread ChatMessage.FromTextWithImageBase64("user", message, imageBase64) }; - await _openAIStreamingClient.StreamChatAsync( + await openAiStreamingClient.StreamChatAsync( model: "gpt-4-vision-preview", messages: messages, maxTokens: 512, diff --git a/BotNet.Services/OpenAI/ThreadTracker.cs b/BotNet.Services/OpenAI/ThreadTracker.cs index 03cf046..0de6f68 100644 --- a/BotNet.Services/OpenAI/ThreadTracker.cs +++ b/BotNet.Services/OpenAI/ThreadTracker.cs @@ -6,8 +6,6 @@ namespace BotNet.Services.OpenAI { public sealed class ThreadTracker( IMemoryCache memoryCache ) { - private readonly IMemoryCache _memoryCache = memoryCache; - public void TrackMessage( long messageId, string sender, @@ -15,7 +13,7 @@ public void TrackMessage( string? imageBase64, long? replyToMessageId ) { - _memoryCache.Set( + memoryCache.Set( key: new MessageId(messageId), value: new Message( Sender: sender, @@ -34,7 +32,7 @@ public void TrackMessage( int maxLines ) { bool firstLine = true; - while (_memoryCache.TryGetValue( + while (memoryCache.TryGetValue( key: new MessageId(messageId), value: out Message? message ) && message != null && maxLines-- > 0) { diff --git a/BotNet.Services/Pemilu2024/PilegDPRDapilDataSource.cs b/BotNet.Services/Pemilu2024/PilegDPRDapilDataSource.cs index 75a613b..e212647 100644 --- a/BotNet.Services/Pemilu2024/PilegDPRDapilDataSource.cs +++ b/BotNet.Services/Pemilu2024/PilegDPRDapilDataSource.cs @@ -7,44 +7,42 @@ using BotNet.Services.Sqlite; namespace BotNet.Services.Pemilu2024 { - public sealed class PilegDPRDapilDataSource( + public sealed class PilegDprDapilDataSource( ScopedDatabase scopedDatabase, SirekapClient sirekapClient ) : IScopedDataSource { - private const string PKB = "1"; - private const string GERINDRA = "2"; - private const string PDIP = "3"; - private const string GOLKAR = "4"; - private const string NASDEM = "5"; - private const string PARTAI_BURUH = "6"; - private const string GELORA = "7"; - private const string PKS = "8"; - private const string PKN = "9"; - private const string HANURA = "10"; - private const string GARUDA = "11"; - private const string PAN = "12"; - private const string PBB = "13"; - private const string DEMOKRAT = "14"; - private const string PSI = "15"; - private const string PERINDO = "16"; - private const string PPP = "17"; - private const string PNA = "18"; - private const string GABTHAT = "19"; - private const string PDA = "20"; - private const string PARTAI_ACEH = "21"; - private const string PAS_ACEH = "22"; - private const string PARTAI_SIRA = "23"; - private const string PARTAI_UMMAT = "24"; - private readonly ScopedDatabase _scopedDatabase = scopedDatabase; - private readonly SirekapClient _sirekapClient = sirekapClient; + private const string Pkb = "1"; + private const string Gerindra = "2"; + private const string Pdip = "3"; + private const string Golkar = "4"; + private const string Nasdem = "5"; + private const string PartaiBuruh = "6"; + private const string Gelora = "7"; + private const string Pks = "8"; + private const string Pkn = "9"; + private const string Hanura = "10"; + private const string Garuda = "11"; + private const string Pan = "12"; + private const string Pbb = "13"; + private const string Demokrat = "14"; + private const string Psi = "15"; + private const string Perindo = "16"; + private const string Ppp = "17"; + private const string Pna = "18"; + private const string Gabthat = "19"; + private const string Pda = "20"; + private const string PartaiAceh = "21"; + private const string PasAceh = "22"; + private const string PartaiSira = "23"; + private const string PartaiUmmat = "24"; public string? KodeDapil { get; set; } public async Task LoadTableAsync(CancellationToken cancellationToken) { if (KodeDapil is null) throw new InvalidProgramException("KodeDapil is not set"); - _scopedDatabase.ExecuteNonQuery($$""" - CREATE TABLE pileg_dpr_{{KodeDapil}} ( + scopedDatabase.ExecuteNonQuery($""" + CREATE TABLE pileg_dpr_{KodeDapil} ( partai VARCHAR(50), kode_caleg VARCHAR(10), nomor_urut INTEGER, @@ -55,43 +53,43 @@ jumlah_suara INTEGER ) """); - IDictionary> calegByKodeByKodePartai = await _sirekapClient.GetCalegByKodeByKodePartaiAsync(KodeDapil, cancellationToken); - ReportCalegDPR report = await _sirekapClient.GetReportCalegDPRAsync(KodeDapil, cancellationToken); + IDictionary> calegByKodeByKodePartai = await sirekapClient.GetCalegByKodeByKodePartaiAsync(KodeDapil, cancellationToken); + ReportCalegDpr report = await sirekapClient.GetReportCalegDprAsync(KodeDapil, cancellationToken); foreach ((string kodePartai, IDictionary votesByKodeCaleg) in report.VotesByKodeCalegByKodePartai.OrderBy(pair => pair.Key)) { string partai = kodePartai switch { - PKB => "PKB", - GERINDRA => "Gerindra", - PDIP => "PDIP", - GOLKAR => "Golkar", - NASDEM => "Nasdem", - PARTAI_BURUH => "Partai Buruh", - GELORA => "Gelora", - PKS => "PKS", - PKN => "PKN", - HANURA => "Hanura", - GARUDA => "Garuda", - PAN => "PAN", - PBB => "PBB", - DEMOKRAT => "Demokrat", - PSI => "PSI", - PERINDO => "Perindo", - PPP => "PPP", - PNA => "Partai Nanggroe Aceh", - GABTHAT => "Partai Generasi Atjeh Beusaboh Tha'at Dan Taqwa", - PDA => "Partai Darul Aceh", - PARTAI_ACEH => "Partai Aceh", - PAS_ACEH => "Partai Adil Sejahtera Aceh", - PARTAI_SIRA => "Partai SIRA", - PARTAI_UMMAT => "Partai Ummat", + Pkb => "PKB", + Gerindra => "Gerindra", + Pdip => "PDIP", + Golkar => "Golkar", + Nasdem => "Nasdem", + PartaiBuruh => "Partai Buruh", + Gelora => "Gelora", + Pks => "PKS", + Pkn => "PKN", + Hanura => "Hanura", + Garuda => "Garuda", + Pan => "PAN", + Pbb => "PBB", + Demokrat => "Demokrat", + Psi => "PSI", + Perindo => "Perindo", + Ppp => "PPP", + Pna => "Partai Nanggroe Aceh", + Gabthat => "Partai Generasi Atjeh Beusaboh Tha'at Dan Taqwa", + Pda => "Partai Darul Aceh", + PartaiAceh => "Partai Aceh", + PasAceh => "Partai Adil Sejahtera Aceh", + PartaiSira => "Partai SIRA", + PartaiUmmat => "Partai Ummat", _ => throw new InvalidProgramException("Unknown partai") }; - foreach ((string kodeCaleg, int votes) in votesByKodeCaleg!.OrderBy(pair => pair.Key)) { + foreach ((string kodeCaleg, int votes) in votesByKodeCaleg.OrderBy(pair => pair.Key)) { if (!int.TryParse(kodeCaleg, out _)) continue; Caleg caleg = calegByKodeByKodePartai[kodePartai][kodeCaleg]; - _scopedDatabase.ExecuteNonQuery($$""" - INSERT INTO pileg_dpr_{{KodeDapil}} (partai, kode_caleg, nomor_urut, nama, jenis_kelamin, tempat_tinggal, jumlah_suara) + scopedDatabase.ExecuteNonQuery($""" + INSERT INTO pileg_dpr_{KodeDapil} (partai, kode_caleg, nomor_urut, nama, jenis_kelamin, tempat_tinggal, jumlah_suara) VALUES (@partai, @kode_caleg, @nomor_urut, @nama, @jenis_kelamin, @tempat_tinggal, @jumlah_suara) """, [ @@ -106,7 +104,7 @@ jumlah_suara INTEGER ); } - _scopedDatabase.ExecuteNonQuery($$""" + scopedDatabase.ExecuteNonQuery($$""" INSERT INTO pileg_dpr_{{KodeDapil}} (partai, kode_caleg, nomor_urut, nama, jenis_kelamin, tempat_tinggal, jumlah_suara) VALUES (@partai, null, null, 'Jumlah Suara Total', null, null, @jumlah_suara) """, @@ -116,7 +114,7 @@ jumlah_suara INTEGER ] ); - _scopedDatabase.ExecuteNonQuery($$""" + scopedDatabase.ExecuteNonQuery($$""" INSERT INTO pileg_dpr_{{KodeDapil}} (partai, kode_caleg, nomor_urut, nama, jenis_kelamin, tempat_tinggal, jumlah_suara) VALUES (@partai, null, null, 'Jumlah Suara Partai', null, null, @jumlah_suara) """, diff --git a/BotNet.Services/Pemilu2024/PilegDPRPerDapilDataSource.cs b/BotNet.Services/Pemilu2024/PilegDPRPerDapilDataSource.cs index e13965c..c11ecc1 100644 --- a/BotNet.Services/Pemilu2024/PilegDPRPerDapilDataSource.cs +++ b/BotNet.Services/Pemilu2024/PilegDPRPerDapilDataSource.cs @@ -6,39 +6,37 @@ using BotNet.Services.Sqlite; namespace BotNet.Services.Pemilu2024 { - public sealed class PilegDPRPerDapilDataSource( + public sealed class PilegDprPerDapilDataSource( ScopedDatabase scopedDatabase, SirekapClient sirekapClient ) : IScopedDataSource { - private const string PKB = "1"; - private const string GERINDRA = "2"; - private const string PDIP = "3"; - private const string GOLKAR = "4"; - private const string NASDEM = "5"; - private const string PARTAI_BURUH = "6"; - private const string GELORA = "7"; - private const string PKS = "8"; - private const string PKN = "9"; - private const string HANURA = "10"; - private const string GARUDA = "11"; - private const string PAN = "12"; - private const string PBB = "13"; - private const string DEMOKRAT = "14"; - private const string PSI = "15"; - private const string PERINDO = "16"; - private const string PPP = "17"; - private const string PNA = "18"; - private const string GABTHAT = "19"; - private const string PDA = "20"; - private const string PARTAI_ACEH = "21"; - private const string PAS_ACEH = "22"; - private const string PARTAI_SIRA = "23"; - private const string PARTAI_UMMAT = "24"; - private readonly ScopedDatabase _scopedDatabase = scopedDatabase; - private readonly SirekapClient _sirekapClient = sirekapClient; + private const string Pkb = "1"; + private const string Gerindra = "2"; + private const string Pdip = "3"; + private const string Golkar = "4"; + private const string Nasdem = "5"; + private const string PartaiBuruh = "6"; + private const string Gelora = "7"; + private const string Pks = "8"; + private const string Pkn = "9"; + private const string Hanura = "10"; + private const string Garuda = "11"; + private const string Pan = "12"; + private const string Pbb = "13"; + private const string Demokrat = "14"; + private const string Psi = "15"; + private const string Perindo = "16"; + private const string Ppp = "17"; + private const string Pna = "18"; + private const string Gabthat = "19"; + private const string Pda = "20"; + private const string PartaiAceh = "21"; + private const string PasAceh = "22"; + private const string PartaiSira = "23"; + private const string PartaiUmmat = "24"; public async Task LoadTableAsync(CancellationToken cancellationToken) { - _scopedDatabase.ExecuteNonQuery(""" + scopedDatabase.ExecuteNonQuery(""" CREATE TABLE pileg_dpr_dapil ( kode_dapil VARCHAR(5) PRIMARY KEY, dapil VARCHAR(50), @@ -71,16 +69,16 @@ total INTEGER ) """); - IList listDapilDPR = await _sirekapClient.GetDapilDPRListAsync(cancellationToken); - Dictionary dapilByKode = listDapilDPR.ToDictionary( + IList listDapilDpr = await sirekapClient.GetDapilDprListAsync(cancellationToken); + Dictionary dapilByKode = listDapilDpr.ToDictionary( keySelector: dapil => dapil.Kode ); - ReportPilegDPRByDapil report = await _sirekapClient.GetReportPilegDPRByDapilAsync(cancellationToken); + ReportPilegDprByDapil report = await sirekapClient.GetReportPilegDprByDapilAsync(cancellationToken); - foreach ((string kodeDapil, ReportPilegDPRByDapil.Row? row) in report.RowByKodeDapil.OrderBy(pair => pair.Key)) { + foreach ((string kodeDapil, ReportPilegDprByDapil.Row? row) in report.RowByKodeDapil.OrderBy(pair => pair.Key)) { if (row == null) { - _scopedDatabase.ExecuteNonQuery(""" + scopedDatabase.ExecuteNonQuery(""" INSERT INTO pileg_dpr_dapil (kode_dapil, dapil, progress, pkb, gerindra, pdip, golkar, nasdem, partai_buruh, gelora, pks, pkn, hanura, garuda, pan, pbb, demokrat, psi, perindo, ppp, pna, gabthat, pda, partai_aceh, pas_aceh, partai_sira, partai_ummat, total) VALUES (@kode_dapil, @dapil, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null) """, @@ -92,32 +90,32 @@ INSERT INTO pileg_dpr_dapil (kode_dapil, dapil, progress, pkb, gerindra, pdip, g continue; } - int? pkb = row.VotesByKodePartai!.TryGetValue(PKB, out int p) ? p : null; - int? gerindra = row.VotesByKodePartai!.TryGetValue(GERINDRA, out int g) ? g : null; - int? pdip = row.VotesByKodePartai!.TryGetValue(PDIP, out int pd) ? pd : null; - int? golkar = row.VotesByKodePartai!.TryGetValue(GOLKAR, out int go) ? go : null; - int? nasdem = row.VotesByKodePartai!.TryGetValue(NASDEM, out int n) ? n : null; - int? partai_buruh = row.VotesByKodePartai!.TryGetValue(PARTAI_BURUH, out int pb) ? pb : null; - int? gelora = row.VotesByKodePartai!.TryGetValue(GELORA, out int ge) ? ge : null; - int? pks = row.VotesByKodePartai!.TryGetValue(PKS, out int pk) ? pk : null; - int? pkn = row.VotesByKodePartai!.TryGetValue(PKN, out int pn) ? pn : null; - int? hanura = row.VotesByKodePartai!.TryGetValue(HANURA, out int h) ? h : null; - int? garuda = row.VotesByKodePartai!.TryGetValue(GARUDA, out int ga) ? ga : null; - int? pan = row.VotesByKodePartai!.TryGetValue(PAN, out int pa) ? pa : null; - int? pbb = row.VotesByKodePartai!.TryGetValue(PBB, out int pb2) ? pb2 : null; - int? demokrat = row.VotesByKodePartai!.TryGetValue(DEMOKRAT, out int d) ? d : null; - int? psi = row.VotesByKodePartai!.TryGetValue(PSI, out int ps) ? ps : null; - int? perindo = row.VotesByKodePartai!.TryGetValue(PERINDO, out int pe) ? pe : null; - int? ppp = row.VotesByKodePartai!.TryGetValue(PPP, out int pp) ? pp : null; - int? pna = row.VotesByKodePartai!.TryGetValue(PNA, out int pn2) ? pn2 : null; - int? gabthat = row.VotesByKodePartai!.TryGetValue(GABTHAT, out int gab) ? gab : null; - int? pda = row.VotesByKodePartai!.TryGetValue(PDA, out int pd2) ? pd2 : null; - int? partai_aceh = row.VotesByKodePartai!.TryGetValue(PARTAI_ACEH, out int pa2) ? pa2 : null; - int? pas_aceh = row.VotesByKodePartai!.TryGetValue(PAS_ACEH, out int pas) ? pas : null; - int? partai_sira = row.VotesByKodePartai!.TryGetValue(PARTAI_SIRA, out int s) ? s : null; - int? partai_ummat = row.VotesByKodePartai!.TryGetValue(PARTAI_UMMAT, out int u) ? u : null; - int total = (pkb ?? 0) + (gerindra ?? 0) + (pdip ?? 0) + (golkar ?? 0) + (nasdem ?? 0) + (partai_buruh ?? 0) + (gelora ?? 0) + (pks ?? 0) + (pkn ?? 0) + (hanura ?? 0) + (garuda ?? 0) + (pan ?? 0) + (pbb ?? 0) + (demokrat ?? 0) + (psi ?? 0) + (perindo ?? 0) + (ppp ?? 0) + (pna ?? 0) + (gabthat ?? 0) + (pda ?? 0) + (partai_aceh ?? 0) + (pas_aceh ?? 0) + (partai_sira ?? 0) + (partai_ummat ?? 0); - _scopedDatabase.ExecuteNonQuery(""" + int? pkb = row.VotesByKodePartai!.TryGetValue(Pkb, out int p) ? p : null; + int? gerindra = row.VotesByKodePartai!.TryGetValue(Gerindra, out int g) ? g : null; + int? pdip = row.VotesByKodePartai!.TryGetValue(Pdip, out int pd) ? pd : null; + int? golkar = row.VotesByKodePartai!.TryGetValue(Golkar, out int go) ? go : null; + int? nasdem = row.VotesByKodePartai!.TryGetValue(Nasdem, out int n) ? n : null; + int? partaiBuruh = row.VotesByKodePartai!.TryGetValue(PartaiBuruh, out int pb) ? pb : null; + int? gelora = row.VotesByKodePartai!.TryGetValue(Gelora, out int ge) ? ge : null; + int? pks = row.VotesByKodePartai!.TryGetValue(Pks, out int pk) ? pk : null; + int? pkn = row.VotesByKodePartai!.TryGetValue(Pkn, out int pn) ? pn : null; + int? hanura = row.VotesByKodePartai!.TryGetValue(Hanura, out int h) ? h : null; + int? garuda = row.VotesByKodePartai!.TryGetValue(Garuda, out int ga) ? ga : null; + int? pan = row.VotesByKodePartai!.TryGetValue(Pan, out int pa) ? pa : null; + int? pbb = row.VotesByKodePartai!.TryGetValue(Pbb, out int pb2) ? pb2 : null; + int? demokrat = row.VotesByKodePartai!.TryGetValue(Demokrat, out int d) ? d : null; + int? psi = row.VotesByKodePartai!.TryGetValue(Psi, out int ps) ? ps : null; + int? perindo = row.VotesByKodePartai!.TryGetValue(Perindo, out int pe) ? pe : null; + int? ppp = row.VotesByKodePartai!.TryGetValue(Ppp, out int pp) ? pp : null; + int? pna = row.VotesByKodePartai!.TryGetValue(Pna, out int pn2) ? pn2 : null; + int? gabthat = row.VotesByKodePartai!.TryGetValue(Gabthat, out int gab) ? gab : null; + int? pda = row.VotesByKodePartai!.TryGetValue(Pda, out int pd2) ? pd2 : null; + int? partaiAceh = row.VotesByKodePartai!.TryGetValue(PartaiAceh, out int pa2) ? pa2 : null; + int? pasAceh = row.VotesByKodePartai!.TryGetValue(PasAceh, out int pas) ? pas : null; + int? partaiSira = row.VotesByKodePartai!.TryGetValue(PartaiSira, out int s) ? s : null; + int? partaiUmmat = row.VotesByKodePartai!.TryGetValue(PartaiUmmat, out int u) ? u : null; + int total = (pkb ?? 0) + (gerindra ?? 0) + (pdip ?? 0) + (golkar ?? 0) + (nasdem ?? 0) + (partaiBuruh ?? 0) + (gelora ?? 0) + (pks ?? 0) + (pkn ?? 0) + (hanura ?? 0) + (garuda ?? 0) + (pan ?? 0) + (pbb ?? 0) + (demokrat ?? 0) + (psi ?? 0) + (perindo ?? 0) + (ppp ?? 0) + (pna ?? 0) + (gabthat ?? 0) + (pda ?? 0) + (partaiAceh ?? 0) + (pasAceh ?? 0) + (partaiSira ?? 0) + (partaiUmmat ?? 0); + scopedDatabase.ExecuteNonQuery(""" INSERT INTO pileg_dpr_dapil (kode_dapil, dapil, progress, pkb, gerindra, pdip, golkar, nasdem, partai_buruh, gelora, pks, pkn, hanura, garuda, pan, pbb, demokrat, psi, perindo, ppp, pna, gabthat, pda, partai_aceh, pas_aceh, partai_sira, partai_ummat, total) VALUES (@kode_dapil, @dapil, @progress, @pkb, @gerindra, @pdip, @golkar, @nasdem, @partai_buruh, @gelora, @pks, @pkn, @hanura, @garuda, @pan, @pbb, @demokrat, @psi, @perindo, @ppp, @pna, @gabthat, @pda, @partai_aceh, @pas_aceh, @partai_sira, @partai_ummat, @total) """, @@ -130,7 +128,7 @@ INSERT INTO pileg_dpr_dapil (kode_dapil, dapil, progress, pkb, gerindra, pdip, g ( "@pdip", pdip), ( "@golkar", golkar), ( "@nasdem", nasdem), - ( "@partai_buruh", partai_buruh), + ( "@partai_buruh", partaiBuruh), ( "@gelora", gelora), ( "@pks", pks), ( "@pkn", pkn), @@ -145,10 +143,10 @@ INSERT INTO pileg_dpr_dapil (kode_dapil, dapil, progress, pkb, gerindra, pdip, g ( "@pna", pna), ( "@gabthat", gabthat), ( "@pda", pda), - ( "@partai_aceh", partai_aceh), - ( "@pas_aceh", pas_aceh), - ( "@partai_sira", partai_sira), - ( "@partai_ummat", partai_ummat), + ( "@partai_aceh", partaiAceh), + ( "@pas_aceh", pasAceh), + ( "@partai_sira", partaiSira), + ( "@partai_ummat", partaiUmmat), ( "@total", total) ] ); diff --git a/BotNet.Services/Pemilu2024/PilegDPRPerProvinsiDataSource.cs b/BotNet.Services/Pemilu2024/PilegDPRPerProvinsiDataSource.cs index 386e723..664311c 100644 --- a/BotNet.Services/Pemilu2024/PilegDPRPerProvinsiDataSource.cs +++ b/BotNet.Services/Pemilu2024/PilegDPRPerProvinsiDataSource.cs @@ -6,39 +6,37 @@ using BotNet.Services.Sqlite; namespace BotNet.Services.Pemilu2024 { - public sealed class PilegDPRPerProvinsiDataSource( + public sealed class PilegDprPerProvinsiDataSource( ScopedDatabase scopedDatabase, SirekapClient sirekapClient ) : IScopedDataSource { - private const string PKB = "1"; - private const string GERINDRA = "2"; - private const string PDIP = "3"; - private const string GOLKAR = "4"; - private const string NASDEM = "5"; - private const string PARTAI_BURUH = "6"; - private const string GELORA = "7"; - private const string PKS = "8"; - private const string PKN = "9"; - private const string HANURA = "10"; - private const string GARUDA = "11"; - private const string PAN = "12"; - private const string PBB = "13"; - private const string DEMOKRAT = "14"; - private const string PSI = "15"; - private const string PERINDO = "16"; - private const string PPP = "17"; - private const string PNA = "18"; - private const string GABTHAT = "19"; - private const string PDA = "20"; - private const string PARTAI_ACEH = "21"; - private const string PAS_ACEH = "22"; - private const string PARTAI_SIRA = "23"; - private const string PARTAI_UMMAT = "24"; - private readonly ScopedDatabase _scopedDatabase = scopedDatabase; - private readonly SirekapClient _sirekapClient = sirekapClient; + private const string Pkb = "1"; + private const string Gerindra = "2"; + private const string Pdip = "3"; + private const string Golkar = "4"; + private const string Nasdem = "5"; + private const string PartaiBuruh = "6"; + private const string Gelora = "7"; + private const string Pks = "8"; + private const string Pkn = "9"; + private const string Hanura = "10"; + private const string Garuda = "11"; + private const string Pan = "12"; + private const string Pbb = "13"; + private const string Demokrat = "14"; + private const string Psi = "15"; + private const string Perindo = "16"; + private const string Ppp = "17"; + private const string Pna = "18"; + private const string Gabthat = "19"; + private const string Pda = "20"; + private const string PartaiAceh = "21"; + private const string PasAceh = "22"; + private const string PartaiSira = "23"; + private const string PartaiUmmat = "24"; public async Task LoadTableAsync(CancellationToken cancellationToken) { - _scopedDatabase.ExecuteNonQuery(""" + scopedDatabase.ExecuteNonQuery(""" CREATE TABLE pileg_dpr_provinsi ( provinsi VARCHAR(50) PRIMARY KEY, progress REAL, @@ -70,40 +68,40 @@ total INTEGER ) """); - IList listProvinsi = await _sirekapClient.GetPronvisiListAsync(cancellationToken); + IList listProvinsi = await sirekapClient.GetPronvisiListAsync(cancellationToken); Dictionary provinsiByKode = listProvinsi.ToDictionary( keySelector: provinsi => provinsi.Kode ); - ReportPilegDPRByWilayah report = await _sirekapClient.GetReportPilegDPRByProvinsiAsync(cancellationToken); + ReportPilegDprByWilayah report = await sirekapClient.GetReportPilegDprByProvinsiAsync(cancellationToken); - foreach ((string kodeWilayah, ReportPilegDPRByWilayah.Row row) in report.RowByKodeWilayah.OrderBy(pair => pair.Key)) { - int? pkb = row.VotesByKodePartai!.TryGetValue(PKB, out int p) ? p : null; - int? gerindra = row.VotesByKodePartai!.TryGetValue(GERINDRA, out int g) ? g : null; - int? pdip = row.VotesByKodePartai!.TryGetValue(PDIP, out int pd) ? pd : null; - int? golkar = row.VotesByKodePartai!.TryGetValue(GOLKAR, out int go) ? go : null; - int? nasdem = row.VotesByKodePartai!.TryGetValue(NASDEM, out int n) ? n : null; - int? partai_buruh = row.VotesByKodePartai!.TryGetValue(PARTAI_BURUH, out int pb) ? pb : null; - int? gelora = row.VotesByKodePartai!.TryGetValue(GELORA, out int ge) ? ge : null; - int? pks = row.VotesByKodePartai!.TryGetValue(PKS, out int pk) ? pk : null; - int? pkn = row.VotesByKodePartai!.TryGetValue(PKN, out int pn) ? pn : null; - int? hanura = row.VotesByKodePartai!.TryGetValue(HANURA, out int h) ? h : null; - int? garuda = row.VotesByKodePartai!.TryGetValue(GARUDA, out int ga) ? ga : null; - int? pan = row.VotesByKodePartai!.TryGetValue(PAN, out int pa) ? pa : null; - int? pbb = row.VotesByKodePartai!.TryGetValue(PBB, out int pb2) ? pb2 : null; - int? demokrat = row.VotesByKodePartai!.TryGetValue(DEMOKRAT, out int d) ? d : null; - int? psi = row.VotesByKodePartai!.TryGetValue(PSI, out int ps) ? ps : null; - int? perindo = row.VotesByKodePartai!.TryGetValue(PERINDO, out int pe) ? pe : null; - int? ppp = row.VotesByKodePartai!.TryGetValue(PPP, out int pp) ? pp : null; - int? pna = row.VotesByKodePartai!.TryGetValue(PNA, out int pn2) ? pn2 : null; - int? gabthat = row.VotesByKodePartai!.TryGetValue(GABTHAT, out int gab) ? gab : null; - int? pda = row.VotesByKodePartai!.TryGetValue(PDA, out int pd2) ? pd2 : null; - int? partai_aceh = row.VotesByKodePartai!.TryGetValue(PARTAI_ACEH, out int pa2) ? pa2 : null; - int? pas_aceh = row.VotesByKodePartai!.TryGetValue(PAS_ACEH, out int pas) ? pas : null; - int? partai_sira = row.VotesByKodePartai!.TryGetValue(PARTAI_SIRA, out int s) ? s : null; - int? partai_ummat = row.VotesByKodePartai!.TryGetValue(PARTAI_UMMAT, out int u) ? u: null; - int total = (pkb ?? 0) + (gerindra ?? 0) + (pdip ?? 0) + (golkar ?? 0) + (nasdem ?? 0) + (partai_buruh ?? 0) + (gelora ?? 0) + (pks ?? 0) + (pkn ?? 0) + (hanura ?? 0) + (garuda ?? 0) + (pan ?? 0) + (pbb ?? 0) + (demokrat ?? 0) + (psi ?? 0) + (perindo ?? 0) + (ppp ?? 0) + (pna ?? 0) + (gabthat ?? 0) + (pda ?? 0) + (partai_aceh ?? 0) + (pas_aceh ?? 0) + (partai_sira ?? 0) + (partai_ummat ?? 0); - _scopedDatabase.ExecuteNonQuery(""" + foreach ((string kodeWilayah, ReportPilegDprByWilayah.Row row) in report.RowByKodeWilayah.OrderBy(pair => pair.Key)) { + int? pkb = row.VotesByKodePartai!.TryGetValue(Pkb, out int p) ? p : null; + int? gerindra = row.VotesByKodePartai!.TryGetValue(Gerindra, out int g) ? g : null; + int? pdip = row.VotesByKodePartai!.TryGetValue(Pdip, out int pd) ? pd : null; + int? golkar = row.VotesByKodePartai!.TryGetValue(Golkar, out int go) ? go : null; + int? nasdem = row.VotesByKodePartai!.TryGetValue(Nasdem, out int n) ? n : null; + int? partaiBuruh = row.VotesByKodePartai!.TryGetValue(PartaiBuruh, out int pb) ? pb : null; + int? gelora = row.VotesByKodePartai!.TryGetValue(Gelora, out int ge) ? ge : null; + int? pks = row.VotesByKodePartai!.TryGetValue(Pks, out int pk) ? pk : null; + int? pkn = row.VotesByKodePartai!.TryGetValue(Pkn, out int pn) ? pn : null; + int? hanura = row.VotesByKodePartai!.TryGetValue(Hanura, out int h) ? h : null; + int? garuda = row.VotesByKodePartai!.TryGetValue(Garuda, out int ga) ? ga : null; + int? pan = row.VotesByKodePartai!.TryGetValue(Pan, out int pa) ? pa : null; + int? pbb = row.VotesByKodePartai!.TryGetValue(Pbb, out int pb2) ? pb2 : null; + int? demokrat = row.VotesByKodePartai!.TryGetValue(Demokrat, out int d) ? d : null; + int? psi = row.VotesByKodePartai!.TryGetValue(Psi, out int ps) ? ps : null; + int? perindo = row.VotesByKodePartai!.TryGetValue(Perindo, out int pe) ? pe : null; + int? ppp = row.VotesByKodePartai!.TryGetValue(Ppp, out int pp) ? pp : null; + int? pna = row.VotesByKodePartai!.TryGetValue(Pna, out int pn2) ? pn2 : null; + int? gabthat = row.VotesByKodePartai!.TryGetValue(Gabthat, out int gab) ? gab : null; + int? pda = row.VotesByKodePartai!.TryGetValue(Pda, out int pd2) ? pd2 : null; + int? partaiAceh = row.VotesByKodePartai!.TryGetValue(PartaiAceh, out int pa2) ? pa2 : null; + int? pasAceh = row.VotesByKodePartai!.TryGetValue(PasAceh, out int pas) ? pas : null; + int? partaiSira = row.VotesByKodePartai!.TryGetValue(PartaiSira, out int s) ? s : null; + int? partaiUmmat = row.VotesByKodePartai!.TryGetValue(PartaiUmmat, out int u) ? u: null; + int total = (pkb ?? 0) + (gerindra ?? 0) + (pdip ?? 0) + (golkar ?? 0) + (nasdem ?? 0) + (partaiBuruh ?? 0) + (gelora ?? 0) + (pks ?? 0) + (pkn ?? 0) + (hanura ?? 0) + (garuda ?? 0) + (pan ?? 0) + (pbb ?? 0) + (demokrat ?? 0) + (psi ?? 0) + (perindo ?? 0) + (ppp ?? 0) + (pna ?? 0) + (gabthat ?? 0) + (pda ?? 0) + (partaiAceh ?? 0) + (pasAceh ?? 0) + (partaiSira ?? 0) + (partaiUmmat ?? 0); + scopedDatabase.ExecuteNonQuery(""" INSERT INTO pileg_dpr_provinsi (provinsi, progress, pkb, gerindra, pdip, golkar, nasdem, partai_buruh, gelora, pks, pkn, hanura, garuda, pan, pbb, demokrat, psi, perindo, ppp, pna, gabthat, pda, partai_aceh, pas_aceh, partai_sira, partai_ummat, total) VALUES (@provinsi, @progress, @pkb, @gerindra, @pdip, @golkar, @nasdem, @partai_buruh, @gelora, @pks, @pkn, @hanura, @garuda, @pan, @pbb, @demokrat, @psi, @perindo, @ppp, @pna, @gabthat, @pda, @partai_aceh, @pas_aceh, @partai_sira, @partai_ummat, @total) """, @@ -115,7 +113,7 @@ INSERT INTO pileg_dpr_provinsi (provinsi, progress, pkb, gerindra, pdip, golkar, ( "@pdip", pdip), ( "@golkar", golkar), ( "@nasdem", nasdem), - ( "@partai_buruh", partai_buruh), + ( "@partai_buruh", partaiBuruh), ( "@gelora", gelora), ( "@pks", pks), ( "@pkn", pkn), @@ -130,10 +128,10 @@ INSERT INTO pileg_dpr_provinsi (provinsi, progress, pkb, gerindra, pdip, golkar, ( "@pna", pna), ( "@gabthat", gabthat), ( "@pda", pda), - ( "@partai_aceh", partai_aceh), - ( "@pas_aceh", pas_aceh), - ( "@partai_sira", partai_sira), - ( "@partai_ummat", partai_ummat), + ( "@partai_aceh", partaiAceh), + ( "@pas_aceh", pasAceh), + ( "@partai_sira", partaiSira), + ( "@partai_ummat", partaiUmmat), ( "@total", total) ] ); diff --git a/BotNet.Services/Pemilu2024/PilpresDataSource.cs b/BotNet.Services/Pemilu2024/PilpresDataSource.cs index 8056032..f864153 100644 --- a/BotNet.Services/Pemilu2024/PilpresDataSource.cs +++ b/BotNet.Services/Pemilu2024/PilpresDataSource.cs @@ -10,14 +10,12 @@ public sealed class PilpresDataSource( ScopedDatabase scopedDatabase, SirekapClient sirekapClient ) : IScopedDataSource { - private const string ANIES = "100025"; - private const string PRABOWO = "100026"; - private const string GANJAR = "100027"; - private readonly ScopedDatabase _scopedDatabase = scopedDatabase; - private readonly SirekapClient _sirekapClient = sirekapClient; + private const string Anies = "100025"; + private const string Prabowo = "100026"; + private const string Ganjar = "100027"; public async Task LoadTableAsync(CancellationToken cancellationToken) { - _scopedDatabase.ExecuteNonQuery(""" + scopedDatabase.ExecuteNonQuery(""" CREATE TABLE pilpres ( provinsi VARCHAR(50) PRIMARY KEY, progress REAL, @@ -28,20 +26,20 @@ total INTEGER ) """); - IList listProvinsi = await _sirekapClient.GetPronvisiListAsync(cancellationToken); + IList listProvinsi = await sirekapClient.GetPronvisiListAsync(cancellationToken); Dictionary provinsiByKode = listProvinsi.ToDictionary( keySelector: provinsi => provinsi.Kode ); - ReportPilpres report = await _sirekapClient.GetReportPilpresAsync(cancellationToken); + ReportPilpres report = await sirekapClient.GetReportPilpresAsync(cancellationToken); foreach ((string kodeWilayah, ReportPilpres.Row row) in report.RowByKodeWilayah.OrderBy(pair => pair.Key)) { - int? anies = row.VotesByKodeCalon!.TryGetValue(ANIES, out int a) ? a : null; - int? prabowo = row.VotesByKodeCalon!.TryGetValue(PRABOWO, out int p) ? p : null; - int? ganjar = row.VotesByKodeCalon!.TryGetValue(GANJAR, out int g) ? g : null; + int? anies = row.VotesByKodeCalon!.TryGetValue(Anies, out int a) ? a : null; + int? prabowo = row.VotesByKodeCalon!.TryGetValue(Prabowo, out int p) ? p : null; + int? ganjar = row.VotesByKodeCalon!.TryGetValue(Ganjar, out int g) ? g : null; int total = (anies ?? 0) + (prabowo ?? 0) + (ganjar ?? 0); - _scopedDatabase.ExecuteNonQuery(""" + scopedDatabase.ExecuteNonQuery(""" INSERT INTO pilpres (provinsi, progress, anies, prabowo, ganjar, total) VALUES (@provinsi, @progress, @anies, @prabowo, @ganjar, @total) """, diff --git a/BotNet.Services/Pemilu2024/ServiceCollectionExtensions.cs b/BotNet.Services/Pemilu2024/ServiceCollectionExtensions.cs index 842e042..4a80310 100644 --- a/BotNet.Services/Pemilu2024/ServiceCollectionExtensions.cs +++ b/BotNet.Services/Pemilu2024/ServiceCollectionExtensions.cs @@ -5,100 +5,100 @@ namespace BotNet.Services.Pemilu2024 { public static class ServiceCollectionExtensions { public static IServiceCollection AddPemilu2024(this IServiceCollection services) { services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddKeyedTransient("pilpres"); - services.AddKeyedTransient("pileg_dpr_provinsi"); - services.AddKeyedTransient("pileg_dpr_dapil"); - services.AddPilegDPRDapilDataSource("1101"); - services.AddPilegDPRDapilDataSource("1102"); - services.AddPilegDPRDapilDataSource("5101"); - services.AddPilegDPRDapilDataSource("3601"); - services.AddPilegDPRDapilDataSource("3602"); - services.AddPilegDPRDapilDataSource("3603"); - services.AddPilegDPRDapilDataSource("1701"); - services.AddPilegDPRDapilDataSource("3401"); - services.AddPilegDPRDapilDataSource("3101"); - services.AddPilegDPRDapilDataSource("3102"); - services.AddPilegDPRDapilDataSource("3103"); - services.AddPilegDPRDapilDataSource("7501"); - services.AddPilegDPRDapilDataSource("1501"); - services.AddPilegDPRDapilDataSource("3201"); - services.AddPilegDPRDapilDataSource("3202"); - services.AddPilegDPRDapilDataSource("3203"); - services.AddPilegDPRDapilDataSource("3204"); - services.AddPilegDPRDapilDataSource("3209"); - services.AddPilegDPRDapilDataSource("3205"); - services.AddPilegDPRDapilDataSource("3206"); - services.AddPilegDPRDapilDataSource("3207"); - services.AddPilegDPRDapilDataSource("3208"); - services.AddPilegDPRDapilDataSource("3210"); - services.AddPilegDPRDapilDataSource("3211"); - services.AddPilegDPRDapilDataSource("3301"); - services.AddPilegDPRDapilDataSource("3302"); - services.AddPilegDPRDapilDataSource("3303"); - services.AddPilegDPRDapilDataSource("3304"); - services.AddPilegDPRDapilDataSource("3309"); - services.AddPilegDPRDapilDataSource("3305"); - services.AddPilegDPRDapilDataSource("3306"); - services.AddPilegDPRDapilDataSource("3307"); - services.AddPilegDPRDapilDataSource("3308"); - services.AddPilegDPRDapilDataSource("3310"); - services.AddPilegDPRDapilDataSource("3501"); - services.AddPilegDPRDapilDataSource("3502"); - services.AddPilegDPRDapilDataSource("3503"); - services.AddPilegDPRDapilDataSource("3504"); - services.AddPilegDPRDapilDataSource("3509"); - services.AddPilegDPRDapilDataSource("3505"); - services.AddPilegDPRDapilDataSource("3506"); - services.AddPilegDPRDapilDataSource("3507"); - services.AddPilegDPRDapilDataSource("3508"); - services.AddPilegDPRDapilDataSource("3510"); - services.AddPilegDPRDapilDataSource("3511"); - services.AddPilegDPRDapilDataSource("6101"); - services.AddPilegDPRDapilDataSource("6102"); - services.AddPilegDPRDapilDataSource("6301"); - services.AddPilegDPRDapilDataSource("6302"); - services.AddPilegDPRDapilDataSource("6201"); - services.AddPilegDPRDapilDataSource("6401"); - services.AddPilegDPRDapilDataSource("6501"); - services.AddPilegDPRDapilDataSource("1901"); - services.AddPilegDPRDapilDataSource("2101"); - services.AddPilegDPRDapilDataSource("1801"); - services.AddPilegDPRDapilDataSource("1802"); - services.AddPilegDPRDapilDataSource("8101"); - services.AddPilegDPRDapilDataSource("8201"); - services.AddPilegDPRDapilDataSource("5201"); - services.AddPilegDPRDapilDataSource("5202"); - services.AddPilegDPRDapilDataSource("5301"); - services.AddPilegDPRDapilDataSource("5302"); - services.AddPilegDPRDapilDataSource("9101"); - services.AddPilegDPRDapilDataSource("9201"); - services.AddPilegDPRDapilDataSource("9601"); - services.AddPilegDPRDapilDataSource("9501"); - services.AddPilegDPRDapilDataSource("9301"); - services.AddPilegDPRDapilDataSource("9401"); - services.AddPilegDPRDapilDataSource("1401"); - services.AddPilegDPRDapilDataSource("1402"); - services.AddPilegDPRDapilDataSource("7601"); - services.AddPilegDPRDapilDataSource("7301"); - services.AddPilegDPRDapilDataSource("7302"); - services.AddPilegDPRDapilDataSource("7303"); - services.AddPilegDPRDapilDataSource("7201"); - services.AddPilegDPRDapilDataSource("7401"); - services.AddPilegDPRDapilDataSource("7101"); - services.AddPilegDPRDapilDataSource("1301"); - services.AddPilegDPRDapilDataSource("1302"); - services.AddPilegDPRDapilDataSource("1601"); - services.AddPilegDPRDapilDataSource("1602"); - services.AddPilegDPRDapilDataSource("1201"); - services.AddPilegDPRDapilDataSource("1202"); - services.AddPilegDPRDapilDataSource("1203"); + services.AddKeyedTransient("pileg_dpr_provinsi"); + services.AddKeyedTransient("pileg_dpr_dapil"); + services.AddPilegDprDapilDataSource("1101"); + services.AddPilegDprDapilDataSource("1102"); + services.AddPilegDprDapilDataSource("5101"); + services.AddPilegDprDapilDataSource("3601"); + services.AddPilegDprDapilDataSource("3602"); + services.AddPilegDprDapilDataSource("3603"); + services.AddPilegDprDapilDataSource("1701"); + services.AddPilegDprDapilDataSource("3401"); + services.AddPilegDprDapilDataSource("3101"); + services.AddPilegDprDapilDataSource("3102"); + services.AddPilegDprDapilDataSource("3103"); + services.AddPilegDprDapilDataSource("7501"); + services.AddPilegDprDapilDataSource("1501"); + services.AddPilegDprDapilDataSource("3201"); + services.AddPilegDprDapilDataSource("3202"); + services.AddPilegDprDapilDataSource("3203"); + services.AddPilegDprDapilDataSource("3204"); + services.AddPilegDprDapilDataSource("3209"); + services.AddPilegDprDapilDataSource("3205"); + services.AddPilegDprDapilDataSource("3206"); + services.AddPilegDprDapilDataSource("3207"); + services.AddPilegDprDapilDataSource("3208"); + services.AddPilegDprDapilDataSource("3210"); + services.AddPilegDprDapilDataSource("3211"); + services.AddPilegDprDapilDataSource("3301"); + services.AddPilegDprDapilDataSource("3302"); + services.AddPilegDprDapilDataSource("3303"); + services.AddPilegDprDapilDataSource("3304"); + services.AddPilegDprDapilDataSource("3309"); + services.AddPilegDprDapilDataSource("3305"); + services.AddPilegDprDapilDataSource("3306"); + services.AddPilegDprDapilDataSource("3307"); + services.AddPilegDprDapilDataSource("3308"); + services.AddPilegDprDapilDataSource("3310"); + services.AddPilegDprDapilDataSource("3501"); + services.AddPilegDprDapilDataSource("3502"); + services.AddPilegDprDapilDataSource("3503"); + services.AddPilegDprDapilDataSource("3504"); + services.AddPilegDprDapilDataSource("3509"); + services.AddPilegDprDapilDataSource("3505"); + services.AddPilegDprDapilDataSource("3506"); + services.AddPilegDprDapilDataSource("3507"); + services.AddPilegDprDapilDataSource("3508"); + services.AddPilegDprDapilDataSource("3510"); + services.AddPilegDprDapilDataSource("3511"); + services.AddPilegDprDapilDataSource("6101"); + services.AddPilegDprDapilDataSource("6102"); + services.AddPilegDprDapilDataSource("6301"); + services.AddPilegDprDapilDataSource("6302"); + services.AddPilegDprDapilDataSource("6201"); + services.AddPilegDprDapilDataSource("6401"); + services.AddPilegDprDapilDataSource("6501"); + services.AddPilegDprDapilDataSource("1901"); + services.AddPilegDprDapilDataSource("2101"); + services.AddPilegDprDapilDataSource("1801"); + services.AddPilegDprDapilDataSource("1802"); + services.AddPilegDprDapilDataSource("8101"); + services.AddPilegDprDapilDataSource("8201"); + services.AddPilegDprDapilDataSource("5201"); + services.AddPilegDprDapilDataSource("5202"); + services.AddPilegDprDapilDataSource("5301"); + services.AddPilegDprDapilDataSource("5302"); + services.AddPilegDprDapilDataSource("9101"); + services.AddPilegDprDapilDataSource("9201"); + services.AddPilegDprDapilDataSource("9601"); + services.AddPilegDprDapilDataSource("9501"); + services.AddPilegDprDapilDataSource("9301"); + services.AddPilegDprDapilDataSource("9401"); + services.AddPilegDprDapilDataSource("1401"); + services.AddPilegDprDapilDataSource("1402"); + services.AddPilegDprDapilDataSource("7601"); + services.AddPilegDprDapilDataSource("7301"); + services.AddPilegDprDapilDataSource("7302"); + services.AddPilegDprDapilDataSource("7303"); + services.AddPilegDprDapilDataSource("7201"); + services.AddPilegDprDapilDataSource("7401"); + services.AddPilegDprDapilDataSource("7101"); + services.AddPilegDprDapilDataSource("1301"); + services.AddPilegDprDapilDataSource("1302"); + services.AddPilegDprDapilDataSource("1601"); + services.AddPilegDprDapilDataSource("1602"); + services.AddPilegDprDapilDataSource("1201"); + services.AddPilegDprDapilDataSource("1202"); + services.AddPilegDprDapilDataSource("1203"); return services; } - private static IServiceCollection AddPilegDPRDapilDataSource(this IServiceCollection services, string kodeDapil) { - services.AddKeyedTransient($"pileg_dpr_{kodeDapil}", (serviceProvider, key) => { - PilegDPRDapilDataSource service = serviceProvider.GetRequiredService(); + private static IServiceCollection AddPilegDprDapilDataSource(this IServiceCollection services, string kodeDapil) { + services.AddKeyedTransient($"pileg_dpr_{kodeDapil}", (serviceProvider, _) => { + PilegDprDapilDataSource service = serviceProvider.GetRequiredService(); service.KodeDapil = kodeDapil; return service; }); diff --git a/BotNet.Services/Pemilu2024/SirekapClient.cs b/BotNet.Services/Pemilu2024/SirekapClient.cs index d2220b5..91e951b 100644 --- a/BotNet.Services/Pemilu2024/SirekapClient.cs +++ b/BotNet.Services/Pemilu2024/SirekapClient.cs @@ -4,89 +4,83 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using BotNet.Services.Json; namespace BotNet.Services.Pemilu2024 { public sealed class SirekapClient( HttpClient httpClient ) { - private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { - PropertyNamingPolicy = new SnakeCaseNamingPolicy() - }; - private readonly HttpClient _httpClient = httpClient; - public async Task> GetPaslonByKodeAsync(CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync>( + return await httpClient.GetFromJsonAsync>( requestUri: "https://sirekap-obj-data.kpu.go.id/pemilu/ppwp.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } public async Task>> GetCalegByKodeByKodePartaiAsync(string kodeDapil, CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync>>( + return await httpClient.GetFromJsonAsync>>( requestUri: $"https://sirekap-obj-data.kpu.go.id/pemilu/caleg/partai/{kodeDapil}.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } public async Task> GetPartaiByKodeAsync(CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync>( + return await httpClient.GetFromJsonAsync>( requestUri: "https://sirekap-obj-data.kpu.go.id/pemilu/partai.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } public async Task> GetPronvisiListAsync(CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync>( + return await httpClient.GetFromJsonAsync>( requestUri: "https://sirekap-obj-data.kpu.go.id/wilayah/pemilu/ppwp/0.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } - public async Task> GetDapilDPRListAsync(CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync>( + public async Task> GetDapilDprListAsync(CancellationToken cancellationToken) { + return await httpClient.GetFromJsonAsync>( requestUri: "https://sirekap-obj-data.kpu.go.id/wilayah/pemilu/pdpr/dapil_dpr.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } public async Task> GetSubWilayahListAsync(string kodeWilayah, CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync>( + return await httpClient.GetFromJsonAsync>( requestUri: $"https://sirekap-obj-data.kpu.go.id/wilayah/pemilu/ppwp/{kodeWilayah}.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } public async Task GetReportPilpresAsync(CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync( + return await httpClient.GetFromJsonAsync( requestUri: "https://sirekap-obj-data.kpu.go.id/pemilu/hhcw/ppwp.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } public async Task GetReportPilpresByWilayahAsync(string kodeWilayah, CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync( + return await httpClient.GetFromJsonAsync( requestUri: $"https://sirekap-obj-data.kpu.go.id/pemilu/hhcw/ppwp/{kodeWilayah}.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } - public async Task GetReportPilegDPRByProvinsiAsync(CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync( + public async Task GetReportPilegDprByProvinsiAsync(CancellationToken cancellationToken) { + return await httpClient.GetFromJsonAsync( requestUri: "https://sirekap-obj-data.kpu.go.id/pemilu/hhcw/pdpr.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } - public async Task GetReportPilegDPRByDapilAsync(CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync( + public async Task GetReportPilegDprByDapilAsync(CancellationToken cancellationToken) { + return await httpClient.GetFromJsonAsync( requestUri: "https://sirekap-obj-data.kpu.go.id/pemilu/hhcd/pdpr/0.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); } - public async Task GetReportCalegDPRAsync(string kodeDapil, CancellationToken cancellationToken) { - return await _httpClient.GetFromJsonAsync( + public async Task GetReportCalegDprAsync(string kodeDapil, CancellationToken cancellationToken) { + return await httpClient.GetFromJsonAsync( requestUri: $"https://sirekap-obj-data.kpu.go.id/pemilu/hhcd/pdpr/{kodeDapil}.json", cancellationToken: cancellationToken ) ?? throw new JsonException("Unexpected response"); diff --git a/BotNet.Services/Pemilu2024/Types.cs b/BotNet.Services/Pemilu2024/Types.cs index 5adbfe7..5e77aa9 100644 --- a/BotNet.Services/Pemilu2024/Types.cs +++ b/BotNet.Services/Pemilu2024/Types.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +// ReSharper disable NotAccessedPositionalProperty.Global +// ReSharper disable UnusedMember.Global namespace BotNet.Services.Pemilu2024 { public sealed record Paslon( @@ -75,13 +77,13 @@ int Progres ); } - public sealed record ReportPilegDPRByWilayah( + public sealed record ReportPilegDprByWilayah( [property: JsonPropertyName("ts")] string Timestamp, string Psu, string Mode, IDictionary Chart, - [property: JsonPropertyName("table")] IDictionary RowByKodeWilayah, - ReportPilegDPRByWilayah.Progress Progres + [property: JsonPropertyName("table")] IDictionary RowByKodeWilayah, + ReportPilegDprByWilayah.Progress Progres ) { public sealed record Row { public string? Psu { get; set; } @@ -116,12 +118,12 @@ int Progres ); } - public sealed record ReportPilegDPRByDapil( + public sealed record ReportPilegDprByDapil( [property: JsonPropertyName("ts")] string Timestamp, string Mode, IDictionary Chart, - [property: JsonPropertyName("table")] IDictionary RowByKodeDapil, - ReportPilegDPRByDapil.Progress Progres + [property: JsonPropertyName("table")] IDictionary RowByKodeDapil, + ReportPilegDprByDapil.Progress Progres ) { public sealed record Row { public decimal Persen { get; set; } @@ -154,12 +156,12 @@ int Progres ); } - public sealed record ReportCalegDPR( + public sealed record ReportCalegDpr( [property: JsonPropertyName("ts")] string Timestamp, string Mode, IDictionary Chart, [property: JsonPropertyName("table")] IDictionary> VotesByKodeCalegByKodePartai, - ReportCalegDPR.Progress Progres + ReportCalegDpr.Progress Progres ) { public sealed record Progress( int Total, diff --git a/BotNet.Services/Pesto/Exceptions/PestoAPIException.cs b/BotNet.Services/Pesto/Exceptions/PestoAPIException.cs index df84838..fa47702 100644 --- a/BotNet.Services/Pesto/Exceptions/PestoAPIException.cs +++ b/BotNet.Services/Pesto/Exceptions/PestoAPIException.cs @@ -1,6 +1,7 @@ namespace BotNet.Services.Pesto.Exceptions; -public class PestoAPIException : System.Exception { - public PestoAPIException(string? message) : base(message) { } - public PestoAPIException() : base("Unhandled exception with empty message") { } +public class PestoApiException( + string? message +) : System.Exception(message) { + public PestoApiException() : this("Unhandled exception with empty message") { } } diff --git a/BotNet.Services/Pesto/Exceptions/PestoEmptyCodeException.cs b/BotNet.Services/Pesto/Exceptions/PestoEmptyCodeException.cs index 9ed3de2..a248493 100644 --- a/BotNet.Services/Pesto/Exceptions/PestoEmptyCodeException.cs +++ b/BotNet.Services/Pesto/Exceptions/PestoEmptyCodeException.cs @@ -1,5 +1,3 @@ namespace BotNet.Services.Pesto.Exceptions; -public class PestoEmptyCodeException : System.Exception{ - public PestoEmptyCodeException() : base ("Code parameter is empty") { } -} +public class PestoEmptyCodeException() : System.Exception("Code parameter is empty"); diff --git a/BotNet.Services/Pesto/Exceptions/PestoMonthlyLimitExceededException.cs b/BotNet.Services/Pesto/Exceptions/PestoMonthlyLimitExceededException.cs index fe661be..e2d637e 100644 --- a/BotNet.Services/Pesto/Exceptions/PestoMonthlyLimitExceededException.cs +++ b/BotNet.Services/Pesto/Exceptions/PestoMonthlyLimitExceededException.cs @@ -1,5 +1,3 @@ namespace BotNet.Services.Pesto.Exceptions; -public class PestoMonthlyLimitExceededException : System.Exception { - public PestoMonthlyLimitExceededException() : base("Monthly limit exceeded for current token") { } -} +public class PestoMonthlyLimitExceededException() : System.Exception("Monthly limit exceeded for current token"); diff --git a/BotNet.Services/Pesto/Exceptions/PestoRuntimeNotFoundException.cs b/BotNet.Services/Pesto/Exceptions/PestoRuntimeNotFoundException.cs index c2e1c64..9103f42 100644 --- a/BotNet.Services/Pesto/Exceptions/PestoRuntimeNotFoundException.cs +++ b/BotNet.Services/Pesto/Exceptions/PestoRuntimeNotFoundException.cs @@ -1,6 +1,5 @@ namespace BotNet.Services.Pesto.Exceptions; -public class PestoRuntimeNotFoundException : System.Exception { - public PestoRuntimeNotFoundException(string? runtime) - : base($"Runtime not found for {runtime ?? "current request"}") { } -} +public class PestoRuntimeNotFoundException( + string? runtime +) : System.Exception($"Runtime not found for {runtime ?? "current request"}"); diff --git a/BotNet.Services/Pesto/Exceptions/PestoServerRateLimitedException.cs b/BotNet.Services/Pesto/Exceptions/PestoServerRateLimitedException.cs index 16e0c93..e3978f2 100644 --- a/BotNet.Services/Pesto/Exceptions/PestoServerRateLimitedException.cs +++ b/BotNet.Services/Pesto/Exceptions/PestoServerRateLimitedException.cs @@ -1,5 +1,3 @@ namespace BotNet.Services.Pesto.Exceptions; -public class PestoServerRateLimitedException : System.Exception { - public PestoServerRateLimitedException() : base("Server rate limited") { } -} +public class PestoServerRateLimitedException() : System.Exception("Server rate limited"); diff --git a/BotNet.Services/Pesto/Models/Language.cs b/BotNet.Services/Pesto/Models/Language.cs index 11cd3ee..da3f6dd 100644 --- a/BotNet.Services/Pesto/Models/Language.cs +++ b/BotNet.Services/Pesto/Models/Language.cs @@ -25,13 +25,13 @@ public enum Language { [EnumMember(Value = "Lua")] Lua = 9, [EnumMember(Value = "PHP")] - PHP = 10, + Php = 10, [EnumMember(Value = "Python")] Python = 11, [EnumMember(Value = "Ruby")] Ruby = 12, [EnumMember(Value = "SQLite3")] - SQLite3 = 13, + SqLite3 = 13, [EnumMember(Value = "V")] V = 14 } @@ -49,10 +49,10 @@ public static string ToString(this Language language) => Language.Javascript => "JavaScript", Language.Julia => "Julia", Language.Lua => "Lua", - Language.PHP => "PHP", + Language.Php => "PHP", Language.Python => "Python", Language.Ruby => "Ruby", - Language.SQLite3 => "SQLite3", + Language.SqLite3 => "SQLite3", Language.V => "V", _ => throw new ArgumentOutOfRangeException(nameof(language)) }; diff --git a/BotNet.Services/Pesto/PestoClient.cs b/BotNet.Services/Pesto/PestoClient.cs index e6f6135..9fda7e0 100644 --- a/BotNet.Services/Pesto/PestoClient.cs +++ b/BotNet.Services/Pesto/PestoClient.cs @@ -13,13 +13,13 @@ namespace BotNet.Services.Pesto; -public class PestoClient : IDisposable { - private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { +public sealed class PestoClient : IDisposable { + private static readonly JsonSerializerOptions JsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new JsonStringEnumConverter() } }; - private const int GRACE_PERIOD = 1_500_000; + private const int GracePeriod = 1_500_000; private static SemaphoreSlim? _semaphore; private bool _disposedValue; @@ -53,7 +53,7 @@ ILogger logger _baseUrl = new Uri(options.BaseUrl); _compileTimeout = options.CompileTimeout; _runTimeout = options.RunTimeout; - _executeTimeout = TimeSpan.FromMilliseconds(_compileTimeout + _runTimeout + GRACE_PERIOD); + _executeTimeout = TimeSpan.FromMilliseconds(_compileTimeout + _runTimeout + GracePeriod); } /// @@ -62,7 +62,7 @@ ILogger logger /// /// PingResponse record which shows some message /// Too many request to the API. Client should try again in a few minutes - /// + /// public async Task PingAsync(CancellationToken cancellationToken) { Uri requestUrl = new(_baseUrl, "api/ping"); using HttpResponseMessage response = await _httpClient.GetAsync( @@ -74,9 +74,9 @@ public async Task PingAsync(CancellationToken cancellationToken) { response.EnsureSuccessStatusCode(); PingResponse? pingResponse = - await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, cancellationToken); + await response.Content.ReadFromJsonAsync(JsonSerializerOptions, cancellationToken); - return pingResponse ?? throw new PestoAPIException(); + return pingResponse ?? throw new PestoApiException(); } /// @@ -88,15 +88,12 @@ public async Task PingAsync(CancellationToken cancellationToken) { /// /// Array of runtimes /// Too many request to the API. Client should try again in a few minutes - /// + /// public async Task ListRuntimesAsync(CancellationToken cancellationToken) { Uri requestUrl = new(_baseUrl, "api/list-runtimes"); - using HttpRequestMessage request = new(HttpMethod.Get, requestUrl) { - Headers = { - { "X-Pesto-Token", _token }, - { "Accept", "application/json" } - } - }; + using HttpRequestMessage request = new(HttpMethod.Get, requestUrl); + request.Headers.Add("X-Pesto-Token", _token); + request.Headers.Add("Accept", "application/json"); using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); @@ -104,9 +101,9 @@ public async Task ListRuntimesAsync(CancellationToken cancellat response.EnsureSuccessStatusCode(); RuntimeResponse? runtimeResponse = - await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, cancellationToken); + await response.Content.ReadFromJsonAsync(JsonSerializerOptions, cancellationToken); - return runtimeResponse ?? throw new PestoAPIException(); + return runtimeResponse ?? throw new PestoApiException(); } /// @@ -117,10 +114,10 @@ public async Task ListRuntimesAsync(CancellationToken cancellat /// /// The code execution result /// Code parameter is empty - /// Token has exceed the allowed monthly limit. User should contact the Pesto team to increase their allowed limit. + /// Token has exceeded the allowed monthly limit. User should contact the Pesto team to increase their allowed limit. /// THe runtime specified (language-version combination) is not allowed at Pesto's API /// Too many request to the API. Client should try again in a few minutes - /// + /// public async Task ExecuteAsync(Language language, string code, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(code)) throw new PestoEmptyCodeException(); @@ -136,28 +133,25 @@ public async Task ExecuteAsync(Language language, string code, Can try { Uri requestUrl = new(_baseUrl, "api/execute"); - using HttpRequestMessage request = new(HttpMethod.Post, requestUrl) { - Headers = { - { "X-Pesto-Token", _token }, - { "Accept", "application/json" } - }, - Content = JsonContent.Create( - inputValue: new CodeRequest( - Language: language, - Code: code, - Version: "latest", - CompileTimeout: _compileTimeout, - RunTimeout: _runTimeout - ), - options: JSON_SERIALIZER_OPTIONS - ) - }; + using HttpRequestMessage request = new(HttpMethod.Post, requestUrl); + request.Headers.Add("X-Pesto-Token", _token); + request.Headers.Add("Accept", "application/json"); + request.Content = JsonContent.Create( + inputValue: new CodeRequest( + Language: language, + Code: code, + Version: "latest", + CompileTimeout: _compileTimeout, + RunTimeout: _runTimeout + ), + options: JsonSerializerOptions + ); using HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, linkedSource.Token).ConfigureAwait(false); if (response.StatusCode != HttpStatusCode.OK) { ErrorResponse? errorResponse = - await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, linkedSource.Token); + await response.Content.ReadFromJsonAsync(JsonSerializerOptions, linkedSource.Token); throw response.StatusCode switch { HttpStatusCode.TooManyRequests when errorResponse?.Message == "Monthly limit exceeded" => @@ -165,21 +159,21 @@ public async Task ExecuteAsync(Language language, string code, Can HttpStatusCode.TooManyRequests => new PestoServerRateLimitedException(), HttpStatusCode.BadRequest when errorResponse?.Message == "Runtime not found" => new PestoRuntimeNotFoundException(language.ToString()), - _ => new PestoAPIException(errorResponse?.Message) + _ => new PestoApiException(errorResponse?.Message) }; } response.EnsureSuccessStatusCode(); CodeResponse? codeResponse = - await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, linkedSource.Token); + await response.Content.ReadFromJsonAsync(JsonSerializerOptions, linkedSource.Token); - return codeResponse ?? throw new PestoAPIException(); + return codeResponse ?? throw new PestoApiException(); } finally { _semaphore.Release(); } } - protected virtual void Dispose(bool disposing) { + private void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { // dispose managed state (managed objects) @@ -194,6 +188,5 @@ protected virtual void Dispose(bool disposing) { public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); - GC.SuppressFinalize(this); } } diff --git a/BotNet.Services/Piston/PistonClient.cs b/BotNet.Services/Piston/PistonClient.cs index cf10ecc..aa4a56a 100644 --- a/BotNet.Services/Piston/PistonClient.cs +++ b/BotNet.Services/Piston/PistonClient.cs @@ -79,7 +79,7 @@ public async Task ExecuteAsync(string language, string code, Canc return await response.Content.ReadFromJsonAsync(_jsonSerializerOptions, cancellationToken) ?? throw new ExecutionEngineException(); #pragma warning restore CS0618 // Type or member is obsolete } finally { - _semaphore!.Release(); + _semaphore.Release(); } } } diff --git a/BotNet.Services/Preview/YoutubePreview.cs b/BotNet.Services/Preview/YoutubePreview.cs index 4e5905e..f3e6056 100644 --- a/BotNet.Services/Preview/YoutubePreview.cs +++ b/BotNet.Services/Preview/YoutubePreview.cs @@ -1,30 +1,27 @@ using System; -using System.Collections.Specialized; using System.Linq; using System.Net.Http; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using System.Web; namespace BotNet.Services.Preview { - public class YoutubePreview : IDisposable { + public sealed class YoutubePreview : IDisposable { private readonly HttpClientHandler _httpClientHandler; private readonly HttpClient _httpClient; - private readonly string _userAgent; private bool _disposedValue; public YoutubePreview() { - _httpClientHandler = new() { - AllowAutoRedirect = false - }; - _userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"; + _httpClientHandler = new() { AllowAutoRedirect = false }; + string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"; _httpClient = new(_httpClientHandler); - _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(_userAgent); + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent); } - public static Uri? ValidateYoutubeLink(string message) { + public static Uri? ValidateYoutubeLink( + string message + ) { return Regex.Matches(message, @"(https?://)?(www.)?youtube.com/watch\?v=[a-zA-Z0-9_-]+") .Select(match => new Uri(match.Value)) .FirstOrDefault(); @@ -33,23 +30,26 @@ public YoutubePreview() { /// /// Grid Preview, or in terms of youtube is called storyboard. - /// Generally youtube will fetch image when the progress bar is hovered. But fortunately, - /// youtube response gave us clue. + /// Generally YouTube will fetch image when the progress bar is hovered. But fortunately, + /// YouTube response gave us clue. /// We can get the image from JSON inside their javascript. We only need this link - /// eg: https://i.ytimg.com/sb//storyboard3_L$L/$N.jpg + /// eg: https://i.ytimg.com/sb/<id>/storyboard3_L$L/$N.jpg /// /// We only need to change the id, $L and $N. /// $L and $N is the grid length, 2 is recommended, so it will be - /// eg: https://i.ytimg.com/sb//storyboard3_L2/M2.jpg + /// eg: https://i.ytimg.com/sb/<id>/storyboard3_L2/M2.jpg /// /// /// /// /// - public async Task YoutubeStoryBoardAsync(Uri youtubeLink, CancellationToken cancellationToken) { + public async Task YoutubeStoryBoardAsync( + Uri youtubeLink, + CancellationToken cancellationToken + ) { using HttpResponseMessage response = await _httpClient.GetAsync(youtubeLink.ToString(), cancellationToken); response.EnsureSuccessStatusCode(); - string responseBody = await response.Content.ReadAsStringAsync(); + string responseBody = await response.Content.ReadAsStringAsync(cancellationToken); string jsPattern = @"[^<]*ytInitialPlayerResponse\s*=\s*({.*?});[^<]*<\/script>"; Match match = Regex.Match(responseBody, jsPattern, RegexOptions.Singleline); @@ -58,7 +58,9 @@ public async Task YoutubeStoryBoardAsync(Uri youtubeLink, CancellationToken throw new InvalidOperationException("Failed to get preview image"); } - string jsonData = match.Groups[1].Value.Trim(); + string jsonData = match.Groups[1] + .Value + .Trim(); if (string.IsNullOrWhiteSpace(jsonData)) { throw new InvalidOperationException("Failed to get JSON data"); } @@ -77,33 +79,31 @@ public async Task YoutubeStoryBoardAsync(Uri youtubeLink, CancellationToken Uri uri = new(storyBoardsLink); - // The "spec" key from storyboard is having a query string with "|" (pipe) delimiter. - // Somehow the Uri class cannot read "|" (pipe) delimiter and make the query string chopped. - string queryString = storyBoardsLink.TrimStart('?'); - queryString = queryString.Replace('|', '&'); - // Parse the query string manually - NameValueCollection queryParams = HttpUtility.ParseQueryString(queryString); - // We need to take last dynamically generated "sigh" key from the querystring - string sighQueryKey = storyBoardsLink.Split(@"rs$").Last(); + string sighQueryKey = storyBoardsLink.Split("rs$") + .Last(); // We need to take last dynamically generated "sqp" key from the querystring - string sqpQueryKey = storyBoardsLink.Split('|').First(); + string sqpQueryKey = storyBoardsLink.Split('|') + .First(); Uri sqp = new(sqpQueryKey); // Currently only L2 and M2 combination. // The other combination L1 - LN and M1 - MN is need different "sigh" query string - string path = uri.AbsolutePath.Replace("$L", "2").Replace("$N", "M2"); + string path = uri.AbsolutePath + .Replace("$L", "2") + .Replace("$N", "M2"); // Rebuilt the Uri Uri storyboardYoutube = new(uri.Scheme + "://" + uri.Host + path + sqp.Query + "&sigh=rs%24" + sighQueryKey); return storyboardYoutube; - } - protected virtual void Dispose(bool disposing) { + private void Dispose( + bool disposing + ) { if (!_disposedValue) { if (disposing) { // dispose managed state (managed objects) @@ -118,8 +118,6 @@ protected virtual void Dispose(bool disposing) { public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); - GC.SuppressFinalize(this); } } - } diff --git a/BotNet.Services/Primbon/PrimbonScraper.cs b/BotNet.Services/Primbon/PrimbonScraper.cs index 0ef9a4e..a0d753b 100644 --- a/BotNet.Services/Primbon/PrimbonScraper.cs +++ b/BotNet.Services/Primbon/PrimbonScraper.cs @@ -11,12 +11,11 @@ namespace BotNet.Services.Primbon { public class PrimbonScraper( HttpClient httpClient ) { - private const string KAMAROKAM_URL = "https://www.primbon.com/petung_hari_baik.php"; - private const string TALIWANGKE_URL = "https://primbon.com/hari_sangar_taliwangke.php"; - private readonly HttpClient _httpClient = httpClient; + private const string KamarokamUrl = "https://www.primbon.com/petung_hari_baik.php"; + private const string TaliwangkeUrl = "https://primbon.com/hari_sangar_taliwangke.php"; public async Task<(string Title, string[] Traits)> GetKamarokamAsync(DateOnly date, CancellationToken cancellationToken) { - using HttpRequestMessage httpRequest = new(HttpMethod.Post, KAMAROKAM_URL); + using HttpRequestMessage httpRequest = new(HttpMethod.Post, KamarokamUrl); using FormUrlEncodedContent content = new( nameValueCollection: new List>() { new("tgl", date.Day.ToString()), @@ -26,7 +25,7 @@ HttpClient httpClient } ); httpRequest.Content = content; - using HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + using HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken); httpResponse.EnsureSuccessStatusCode(); string html = await httpResponse.Content.ReadAsStringAsync(cancellationToken); @@ -43,7 +42,7 @@ HttpClient httpClient throw new InvalidOperationException("Primbon.com returned an unexpected response."); } - if (traits.IndexOf("") is int index and not -1) { + if (traits.IndexOf("", StringComparison.Ordinal) is int index and not -1) { traits = traits[(index + 4)..]; } @@ -54,7 +53,7 @@ HttpClient httpClient } public async Task<(string JavaneseDate, string Title, string Description)> GetTaliwangkeAsync(DateOnly date, CancellationToken cancellationToken) { - using HttpRequestMessage httpRequest = new(HttpMethod.Post, TALIWANGKE_URL); + using HttpRequestMessage httpRequest = new(HttpMethod.Post, TaliwangkeUrl); using FormUrlEncodedContent content = new( nameValueCollection: new List>() { new("tgl", date.Day.ToString()), @@ -64,7 +63,7 @@ HttpClient httpClient } ); httpRequest.Content = content; - using HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + using HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken); httpResponse.EnsureSuccessStatusCode(); string html = await httpResponse.Content.ReadAsStringAsync(cancellationToken); @@ -83,14 +82,14 @@ HttpClient httpClient throw new InvalidOperationException("Primbon.com returned an unexpected response."); } - if (body.IndexOf("
") is int index1 and not -1) { + if (body.IndexOf("
", StringComparison.Ordinal) is int index1 and not -1) { body = body[(index1 + 4)..]; - if (body.IndexOf("
") is int index2 and not -1) { + if (body.IndexOf("
", StringComparison.Ordinal) is int index2 and not -1) { body = body[..index2]; } } - if (desc.IndexOf("") is int index3 and not -1) { + if (desc.IndexOf("", StringComparison.Ordinal) is int index3 and not -1) { desc = desc[(index3 + 4)..]; } diff --git a/BotNet.Services/ProgrammerHumor/ProgrammerHumorScraper.cs b/BotNet.Services/ProgrammerHumor/ProgrammerHumorScraper.cs index ffcab23..711e688 100644 --- a/BotNet.Services/ProgrammerHumor/ProgrammerHumorScraper.cs +++ b/BotNet.Services/ProgrammerHumor/ProgrammerHumorScraper.cs @@ -7,12 +7,10 @@ namespace BotNet.Services.ProgrammerHumor { public class ProgrammerHumorScraper(HttpClient httpClient) { - private readonly HttpClient _httpClient = httpClient; - public async Task<(string Title, byte[] Image)> GetRandomJokeAsync(CancellationToken cancellationToken) { const string url = "https://programmerhumor.io/?bimber_random_post=true"; using HttpRequestMessage httpRequest = new(HttpMethod.Get, url); - using HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + using HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken); httpResponse.EnsureSuccessStatusCode(); string html = await httpResponse.Content.ReadAsStringAsync(cancellationToken); @@ -26,7 +24,7 @@ public class ProgrammerHumorScraper(HttpClient httpClient) { return ( Title: titleElement?.InnerHtml ?? "Humor", - Image: await _httpClient.GetByteArrayAsync(src, cancellationToken) + Image: await httpClient.GetByteArrayAsync(src, cancellationToken) ); } } diff --git a/BotNet.Services/RateLimit/Internal/PerChatRateLimiter.cs b/BotNet.Services/RateLimit/Internal/PerChatRateLimiter.cs index 670e81a..8fbca25 100644 --- a/BotNet.Services/RateLimit/Internal/PerChatRateLimiter.cs +++ b/BotNet.Services/RateLimit/Internal/PerChatRateLimiter.cs @@ -2,30 +2,23 @@ using System.Collections.Concurrent; namespace BotNet.Services.RateLimit.Internal { - internal class PerChatRateLimiter : RateLimiter { - private readonly int _actionCount; - private readonly TimeSpan _window; - + internal class PerChatRateLimiter( + int actionCount, + TimeSpan window + ) : RateLimiter { private readonly ConcurrentDictionary> _queueByChatId = new(); - public PerChatRateLimiter( - int actionCount, - TimeSpan window - ) { - _actionCount = actionCount; - _window = window; - } - public override void ValidateActionRate(long chatId, long _) { ConcurrentQueue queue = _queueByChatId.GetOrAdd( key: chatId, valueFactory: _ => new ConcurrentQueue() ); + // ReSharper disable once RedundantAssignment DateTime lru = DateTime.Now; while (queue.TryPeek(out lru) - && DateTime.Now - lru > _window + && DateTime.Now - lru > window && queue.TryDequeue(out lru)) { } - if (queue.Count >= _actionCount) throw new RateLimitExceededException(CooldownFormatter.Format(_window - (DateTime.Now - lru))); + if (queue.Count >= actionCount) throw new RateLimitExceededException(CooldownFormatter.Format(window - (DateTime.Now - lru))); queue.Enqueue(DateTime.Now); } } diff --git a/BotNet.Services/RateLimit/Internal/PerUserPerChatRateLimiter.cs b/BotNet.Services/RateLimit/Internal/PerUserPerChatRateLimiter.cs index cbf396c..269ce7a 100644 --- a/BotNet.Services/RateLimit/Internal/PerUserPerChatRateLimiter.cs +++ b/BotNet.Services/RateLimit/Internal/PerUserPerChatRateLimiter.cs @@ -2,30 +2,23 @@ using System.Collections.Concurrent; namespace BotNet.Services.RateLimit.Internal { - internal class PerUserPerChatRateLimiter : RateLimiter { - private readonly int _actionCount; - private readonly TimeSpan _window; - + internal class PerUserPerChatRateLimiter( + int actionCount, + TimeSpan window + ) : RateLimiter { private readonly ConcurrentDictionary<(long ChatId, long UserId), ConcurrentQueue> _queueByChatIdUserId = new(); - public PerUserPerChatRateLimiter( - int actionCount, - TimeSpan window - ) { - _actionCount = actionCount; - _window = window; - } - public override void ValidateActionRate(long chatId, long userId) { ConcurrentQueue queue = _queueByChatIdUserId.GetOrAdd( key: (chatId, userId), valueFactory: _ => new ConcurrentQueue() ); + // ReSharper disable once RedundantAssignment DateTime lru = DateTime.Now; while (queue.TryPeek(out lru) - && DateTime.Now - lru > _window + && DateTime.Now - lru > window && queue.TryDequeue(out lru)) { } - if (queue.Count >= _actionCount) throw new RateLimitExceededException(CooldownFormatter.Format(_window - (DateTime.Now - lru))); + if (queue.Count >= actionCount) throw new RateLimitExceededException(CooldownFormatter.Format(window - (DateTime.Now - lru))); queue.Enqueue(DateTime.Now); } } diff --git a/BotNet.Services/RateLimit/Internal/PerUserRateLimiter.cs b/BotNet.Services/RateLimit/Internal/PerUserRateLimiter.cs index 818c735..e60d610 100644 --- a/BotNet.Services/RateLimit/Internal/PerUserRateLimiter.cs +++ b/BotNet.Services/RateLimit/Internal/PerUserRateLimiter.cs @@ -2,30 +2,23 @@ using System.Collections.Concurrent; namespace BotNet.Services.RateLimit.Internal { - internal class PerUserRateLimiter : RateLimiter { - private readonly int _actionCount; - private readonly TimeSpan _window; - + internal class PerUserRateLimiter( + int actionCount, + TimeSpan window + ) : RateLimiter { private readonly ConcurrentDictionary> _queueByUserId = new(); - public PerUserRateLimiter( - int actionCount, - TimeSpan window - ) { - _actionCount = actionCount; - _window = window; - } - public override void ValidateActionRate(long _, long userId) { ConcurrentQueue queue = _queueByUserId.GetOrAdd( key: userId, valueFactory: _ => new ConcurrentQueue() ); + // ReSharper disable once RedundantAssignment DateTime lru = DateTime.Now; while (queue.TryPeek(out lru) - && DateTime.Now - lru > _window + && DateTime.Now - lru > window && queue.TryDequeue(out lru)) { } - if (queue.Count >= _actionCount) throw new RateLimitExceededException(CooldownFormatter.Format(_window - (DateTime.Now - lru))); + if (queue.Count >= actionCount) throw new RateLimitExceededException(CooldownFormatter.Format(window - (DateTime.Now - lru))); queue.Enqueue(DateTime.Now); } } diff --git a/BotNet.Services/RateLimit/RateLimitExceededException.cs b/BotNet.Services/RateLimit/RateLimitExceededException.cs index f050e7c..3d8715c 100644 --- a/BotNet.Services/RateLimit/RateLimitExceededException.cs +++ b/BotNet.Services/RateLimit/RateLimitExceededException.cs @@ -1,11 +1,9 @@ using System; namespace BotNet.Services.RateLimit { - public class RateLimitExceededException : Exception { - public string Cooldown { get; } - - public RateLimitExceededException(string cooldown) { - Cooldown = cooldown; - } + public class RateLimitExceededException( + string cooldown + ) : Exception { + public string Cooldown { get; } = cooldown; } } diff --git a/BotNet.Services/RateLimit/RateLimiter.cs b/BotNet.Services/RateLimit/RateLimiter.cs index c924bff..8d69ce4 100644 --- a/BotNet.Services/RateLimit/RateLimiter.cs +++ b/BotNet.Services/RateLimit/RateLimiter.cs @@ -6,8 +6,6 @@ namespace BotNet.Services.RateLimit { /// Note: This rate limiter is for monolith app and isn't ready to be used in Orleans ///
public abstract class RateLimiter { - protected RateLimiter() { } - public static RateLimiter PerChat(int actionCount, TimeSpan window) => new PerChatRateLimiter(actionCount, window); public static RateLimiter PerUser(int actionCount, TimeSpan window) => new PerUserRateLimiter(actionCount, window); public static RateLimiter PerUserPerChat(int actionCount, TimeSpan window) => new PerUserPerChatRateLimiter(actionCount, window); diff --git a/BotNet.Services/Sqlite/ScopedDatabase.cs b/BotNet.Services/Sqlite/ScopedDatabase.cs index c89dd0d..e538501 100644 --- a/BotNet.Services/Sqlite/ScopedDatabase.cs +++ b/BotNet.Services/Sqlite/ScopedDatabase.cs @@ -57,7 +57,6 @@ private void Dispose(bool disposing) { public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); - GC.SuppressFinalize(this); } } } diff --git a/BotNet.Services/Stability/Models/ContentFilteredException.cs b/BotNet.Services/Stability/Models/ContentFilteredException.cs index 701b284..5e900a9 100644 --- a/BotNet.Services/Stability/Models/ContentFilteredException.cs +++ b/BotNet.Services/Stability/Models/ContentFilteredException.cs @@ -2,10 +2,10 @@ namespace BotNet.Services.Stability.Models { public sealed class ContentFilteredException : Exception { - public ContentFilteredException() { - } + public ContentFilteredException() { } - public ContentFilteredException(string? message) : base(message) { - } + public ContentFilteredException( + string? message + ) : base(message) { } } } diff --git a/BotNet.Services/Stability/Skills/ImageGenerationBot.cs b/BotNet.Services/Stability/Skills/ImageGenerationBot.cs index c8d0a42..13063e2 100644 --- a/BotNet.Services/Stability/Skills/ImageGenerationBot.cs +++ b/BotNet.Services/Stability/Skills/ImageGenerationBot.cs @@ -5,13 +5,11 @@ namespace BotNet.Services.Stability.Skills { public sealed class ImageGenerationBot( StabilityClient stabilityClient ) { - private readonly StabilityClient _stabilityClient = stabilityClient; - public async Task GenerateImageAsync( string prompt, CancellationToken cancellationToken ) { - return await _stabilityClient.GenerateImageAsync( + return await stabilityClient.GenerateImageAsync( engine: "stable-diffusion-xl-1024-v1-0", promptText: prompt, cancellationToken: cancellationToken diff --git a/BotNet.Services/Stability/Skills/ImageVariationBot.cs b/BotNet.Services/Stability/Skills/ImageVariationBot.cs index 2ace9bb..5e572df 100644 --- a/BotNet.Services/Stability/Skills/ImageVariationBot.cs +++ b/BotNet.Services/Stability/Skills/ImageVariationBot.cs @@ -5,14 +5,12 @@ namespace BotNet.Services.Stability.Skills { public sealed class ImageVariationBot( StabilityClient stabilityClient ) { - private readonly StabilityClient _stabilityClient = stabilityClient; - public async Task ModifyImageAsync( byte[] image, string prompt, CancellationToken cancellationToken ) { - return await _stabilityClient.ModifyImageAsync( + return await stabilityClient.ModifyImageAsync( engine: "stable-diffusion-xl-1024-v1-0", promptImage: image, promptText: prompt, diff --git a/BotNet.Services/Stability/StabilityClient.cs b/BotNet.Services/Stability/StabilityClient.cs index 48a7d18..e828894 100644 --- a/BotNet.Services/Stability/StabilityClient.cs +++ b/BotNet.Services/Stability/StabilityClient.cs @@ -16,25 +16,24 @@ public sealed class StabilityClient( IOptions optionsAccessor, ILogger logger ) { - private const string TEXT_TO_IMAGE_URL_TEMPLATE = "https://api.stability.ai/v1/generation/{0}/text-to-image"; - private const string IMAGE_TO_IMAGE_URL_TEMPLATE = "https://api.stability.ai/v1/generation/{0}/image-to-image"; + private const string TextToImageUrlTemplate = "https://api.stability.ai/v1/generation/{0}/text-to-image"; + private const string ImageToImageUrlTemplate = "https://api.stability.ai/v1/generation/{0}/image-to-image"; - private static readonly JsonSerializerOptions SNAKE_CASE_SERIALIZER_OPTIONS = new() { + private static readonly JsonSerializerOptions SnakeCaseSerializerOptions = new() { PropertyNamingPolicy = new SnakeCaseNamingPolicy() }; - private static readonly JsonSerializerOptions CAMEL_CASE_SERIALIZER_OPTIONS = new() { + private static readonly JsonSerializerOptions CamelCaseSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - private readonly HttpClient _httpClient = httpClient; + private readonly string? _apiKey = optionsAccessor.Value.ApiKey; - private readonly ILogger _logger = logger; public async Task GenerateImageAsync( string engine, string promptText, CancellationToken cancellationToken ) { - string url = string.Format(TEXT_TO_IMAGE_URL_TEMPLATE, engine); + string url = string.Format(TextToImageUrlTemplate, engine); using HttpRequestMessage request = new(HttpMethod.Post, url); request.Headers.Add("Authorization", $"Bearer {_apiKey}"); request.Headers.Add("Accept", "application/json"); @@ -57,28 +56,28 @@ CancellationToken cancellationToken } } }, - options: SNAKE_CASE_SERIALIZER_OPTIONS + options: SnakeCaseSerializerOptions ); - using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { string error = await response.Content.ReadAsStringAsync(cancellationToken); if (response.StatusCode == HttpStatusCode.BadRequest) { - ErrorResponse? errorResponse = JsonSerializer.Deserialize(error, SNAKE_CASE_SERIALIZER_OPTIONS); - throw new ContentFilteredException(errorResponse?.Message); + ErrorResponse? errorResponse = JsonSerializer.Deserialize(error, SnakeCaseSerializerOptions); + throw new ContentFilteredException(errorResponse?.Message ?? "Content filtered"); } - _logger.LogError("Unable to generate image: {0}, HTTP Status Code: {1}", error, (int)response.StatusCode); + logger.LogError("Unable to generate image: {0}, HTTP Status Code: {1}", error, (int)response.StatusCode); response.EnsureSuccessStatusCode(); } string responseJson = await response.Content.ReadAsStringAsync(cancellationToken); - TextToImageResponse? responseData = JsonSerializer.Deserialize(responseJson, CAMEL_CASE_SERIALIZER_OPTIONS); + TextToImageResponse? responseData = JsonSerializer.Deserialize(responseJson, CamelCaseSerializerOptions); - if (responseData is { Artifacts: [Artifact { FinishReason: "CONTENT_FILTERED" }] }) { + if (responseData is { Artifacts: [{ FinishReason: "CONTENT_FILTERED" }] }) { throw new ContentFilteredException(); } - if (responseData is not { Artifacts: [Artifact { FinishReason: "SUCCESS", Base64: var base64 }] }) { + if (responseData is not { Artifacts: [{ FinishReason: "SUCCESS", Base64: var base64 }] }) { throw new HttpRequestException(); } @@ -91,7 +90,7 @@ public async Task ModifyImageAsync( string promptText, CancellationToken cancellationToken ) { - string url = string.Format(IMAGE_TO_IMAGE_URL_TEMPLATE, engine); + string url = string.Format(ImageToImageUrlTemplate, engine); using HttpRequestMessage request = new(HttpMethod.Post, url); request.Headers.Add("Authorization", $"Bearer {_apiKey}"); request.Headers.Add("Accept", "application/json"); @@ -127,22 +126,22 @@ CancellationToken cancellationToken formData.Add(textPrompts1Text, "text_prompts[1][text]"); formData.Add(textPrompts1Weight, "text_prompts[1][weight]"); request.Content = formData; - using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { string error = await response.Content.ReadAsStringAsync(cancellationToken); - _logger.LogError("Unable to generate image: {0}, HTTP Status Code: {1}", error, (int)response.StatusCode); + logger.LogError("Unable to generate image: {0}, HTTP Status Code: {1}", error, (int)response.StatusCode); response.EnsureSuccessStatusCode(); } string responseJson = await response.Content.ReadAsStringAsync(cancellationToken); - TextToImageResponse? responseData = JsonSerializer.Deserialize(responseJson, CAMEL_CASE_SERIALIZER_OPTIONS); + TextToImageResponse? responseData = JsonSerializer.Deserialize(responseJson, CamelCaseSerializerOptions); - if (responseData is { Artifacts: [Artifact { FinishReason: "CONTENT_FILTERED" }] }) { + if (responseData is { Artifacts: [{ FinishReason: "CONTENT_FILTERED" }] }) { throw new ContentFilteredException(); } - if (responseData is not { Artifacts: [Artifact { FinishReason: "SUCCESS", Base64: var base64 }] }) { + if (responseData is not { Artifacts: [{ FinishReason: "SUCCESS", Base64: var base64 }] }) { throw new HttpRequestException(); } diff --git a/BotNet.Services/TelegramClient/TelegramBotClientResilienceExtensions.cs b/BotNet.Services/TelegramClient/TelegramBotClientResilienceExtensions.cs index 9f512c1..10181e8 100644 --- a/BotNet.Services/TelegramClient/TelegramBotClientResilienceExtensions.cs +++ b/BotNet.Services/TelegramClient/TelegramBotClientResilienceExtensions.cs @@ -38,9 +38,7 @@ public static async Task SendTextMessageAsync( replyMarkup: replyMarkup, cancellationToken: cancellationToken ); - } catch (ApiRequestException) { - continue; - } + } catch (ApiRequestException) { } } // Last resort: escape everything @@ -81,9 +79,7 @@ public static async Task EditMessageTextAsync( replyMarkup: replyMarkup, cancellationToken: cancellationToken ); - } catch (ApiRequestException) { - continue; - } + } catch (ApiRequestException) { } } // Last resort: escape everything diff --git a/BotNet.Services/ThisXDoesNotExist/ThisArtworkDoesNotExist.cs b/BotNet.Services/ThisXDoesNotExist/ThisArtworkDoesNotExist.cs index 9acb701..d769dcf 100644 --- a/BotNet.Services/ThisXDoesNotExist/ThisArtworkDoesNotExist.cs +++ b/BotNet.Services/ThisXDoesNotExist/ThisArtworkDoesNotExist.cs @@ -3,19 +3,13 @@ using System.Threading.Tasks; namespace BotNet.Services.ThisXDoesNotExist { - public class ThisArtworkDoesNotExist { - private readonly HttpClient _httpClient; - - public ThisArtworkDoesNotExist( - HttpClient httpClient - ) { - _httpClient = httpClient; - } - + public class ThisArtworkDoesNotExist( + HttpClient httpClient + ) { public async Task GetRandomArtworkAsync(CancellationToken cancellationToken) { const string url = "https://thisartworkdoesnotexist.com/"; using HttpRequestMessage httpRequest = new(HttpMethod.Get, url); - using HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + using HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken); httpResponse.EnsureSuccessStatusCode(); return await httpResponse.Content.ReadAsByteArrayAsync(cancellationToken); diff --git a/BotNet.Services/ThisXDoesNotExist/ThisCatDoesNotExist.cs b/BotNet.Services/ThisXDoesNotExist/ThisCatDoesNotExist.cs index 97ff916..5b9b48d 100644 --- a/BotNet.Services/ThisXDoesNotExist/ThisCatDoesNotExist.cs +++ b/BotNet.Services/ThisXDoesNotExist/ThisCatDoesNotExist.cs @@ -3,19 +3,13 @@ using System.Threading.Tasks; namespace BotNet.Services.ThisXDoesNotExist { - public class ThisCatDoesNotExist { - private readonly HttpClient _httpClient; - - public ThisCatDoesNotExist( - HttpClient httpClient - ) { - _httpClient = httpClient; - } - + public class ThisCatDoesNotExist( + HttpClient httpClient + ) { public async Task GetRandomCatImageAsync(CancellationToken cancellationToken) { const string url = "https://thiscatdoesnotexist.com/"; using HttpRequestMessage httpRequest = new(HttpMethod.Get, url); - using HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + using HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken); httpResponse.EnsureSuccessStatusCode(); return await httpResponse.Content.ReadAsByteArrayAsync(cancellationToken); diff --git a/BotNet.Services/ThisXDoesNotExist/ThisIdeaDoesNotExist.cs b/BotNet.Services/ThisXDoesNotExist/ThisIdeaDoesNotExist.cs index 1228bc7..76226df 100644 --- a/BotNet.Services/ThisXDoesNotExist/ThisIdeaDoesNotExist.cs +++ b/BotNet.Services/ThisXDoesNotExist/ThisIdeaDoesNotExist.cs @@ -7,12 +7,10 @@ namespace BotNet.Services.ThisXDoesNotExist { public class ThisIdeaDoesNotExist(HttpClient httpClient) { - private readonly HttpClient _httpClient = httpClient; - public async Task GetRandomIdeaAsync(CancellationToken cancellationToken) { const string url = "https://thisideadoesnotexist.com/"; using HttpRequestMessage httpRequest = new(HttpMethod.Get, url); - using HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + using HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken); httpResponse.EnsureSuccessStatusCode(); string html = await httpResponse.Content.ReadAsStringAsync(cancellationToken); diff --git a/BotNet.Services/Tiktok/TiktokLinkSanitizer.cs b/BotNet.Services/Tiktok/TiktokLinkSanitizer.cs index 1b01834..db26da7 100644 --- a/BotNet.Services/Tiktok/TiktokLinkSanitizer.cs +++ b/BotNet.Services/Tiktok/TiktokLinkSanitizer.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace BotNet.Services.Tiktok { - public class TiktokLinkSanitizer : IDisposable { + public sealed class TiktokLinkSanitizer : IDisposable { private readonly HttpClientHandler _httpClientHandler; private readonly HttpClient _httpClient; private bool _disposedValue; @@ -34,7 +34,7 @@ public async Task SanitizeAsync(Uri link, CancellationToken cancellationTok .FirstOrDefault(); } - protected virtual void Dispose(bool disposing) { + private void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { // dispose managed state (managed objects) @@ -49,7 +49,6 @@ protected virtual void Dispose(bool disposing) { public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); - GC.SuppressFinalize(this); } } } diff --git a/BotNet.Services/Tokopedia/TokopediaLinkSanitizer.cs b/BotNet.Services/Tokopedia/TokopediaLinkSanitizer.cs index 7c7a081..f708cd5 100644 --- a/BotNet.Services/Tokopedia/TokopediaLinkSanitizer.cs +++ b/BotNet.Services/Tokopedia/TokopediaLinkSanitizer.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace BotNet.Services.Tokopedia { - public class TokopediaLinkSanitizer : IDisposable { + public sealed class TokopediaLinkSanitizer : IDisposable { private readonly HttpClientHandler _httpClientHandler; private readonly HttpClient _httpClient; private readonly string _userAgent; @@ -52,13 +52,13 @@ public async Task SanitizeAsync(Uri link, CancellationToken cancellationTok throw new HttpRequestException("Invalid link"); } - Uri? redirectedLink = secondStageRedirect; + Uri redirectedLink = secondStageRedirect; string sanitizedUri = redirectedLink.GetLeftPart(UriPartial.Path); return new Uri(sanitizedUri); } - protected virtual void Dispose(bool disposing) { + private void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { // dispose managed state (managed objects) @@ -72,7 +72,6 @@ protected virtual void Dispose(bool disposing) { public void Dispose() { Dispose(disposing: true); - GC.SuppressFinalize(this); } } } diff --git a/BotNet.Services/Typography/BotNetFontService.cs b/BotNet.Services/Typography/BotNetFontService.cs index 489bd99..314e993 100644 --- a/BotNet.Services/Typography/BotNetFontService.cs +++ b/BotNet.Services/Typography/BotNetFontService.cs @@ -5,15 +5,15 @@ namespace BotNet.Services.Typography { public class BotNetFontService { - private readonly FontFamily _jetbrainsMonoNL = new( + private readonly FontFamily _jetbrainsMonoNl = new( name: "JetBrainsMonoNL", - stylesSetup: EnumerateJetBrainsMonoMLStyles); + stylesSetup: EnumerateJetBrainsMonoNlStyles); private readonly FontFamily _inter = new( name: "Inter", stylesSetup: EnumerateInterStyles); - private static IEnumerable EnumerateJetBrainsMonoMLStyles(FontFamily fontFamily) { + private static IEnumerable EnumerateJetBrainsMonoNlStyles(FontFamily fontFamily) { Assembly resourceAssembly = Assembly.GetAssembly(typeof(BotNetFontService))!; string resourceNamespace = "BotNet.Services.Typography.Assets"; @@ -74,11 +74,11 @@ FontStyle CreateFontStyle(string name, int weight, FontStyleType styleType) { yield return CreateFontStyle("Inter-Black", 900, FontStyleType.Normal); } - public FontStyle GetDefaultFontStyle() => _jetbrainsMonoNL.GetFontStyles().Single(style => style is { Weight: 400, StyleType: FontStyleType.Normal }); - public FontFamily[] GetFontFamilies() => new[] { _jetbrainsMonoNL, _inter }; + public FontStyle GetDefaultFontStyle() => _jetbrainsMonoNl.GetFontStyles().Single(style => style is { Weight: 400, StyleType: FontStyleType.Normal }); + public FontFamily[] GetFontFamilies() => [_jetbrainsMonoNl, _inter]; public FontStyle GetFontStyleById(string id) - => _jetbrainsMonoNL.GetFontStyles().SingleOrDefault(style => style.Id == id) + => _jetbrainsMonoNl.GetFontStyles().SingleOrDefault(style => style.Id == id) ?? _inter.GetFontStyles().SingleOrDefault(style => style.Id == id) ?? throw new KeyNotFoundException(); } diff --git a/BotNet.Services/Typography/FontStyle.cs b/BotNet.Services/Typography/FontStyle.cs index ca6c5cb..b1b860d 100644 --- a/BotNet.Services/Typography/FontStyle.cs +++ b/BotNet.Services/Typography/FontStyle.cs @@ -37,7 +37,7 @@ string resourceName _resourceName = resourceName; } - public int CompareTo(FontStyle? other) => Id.CompareTo(other?.Id ?? throw new ArgumentNullException(nameof(other))); + public int CompareTo(FontStyle? other) => String.Compare(Id, other?.Id ?? throw new ArgumentNullException(nameof(other)), StringComparison.Ordinal); public Stream OpenStream() => _resourceAssembly.GetManifestResourceStream(_resourceName) ?? throw new InvalidOperationException("Embedded resource could not be loaded."); } diff --git a/BotNet.Services/Weather/CurrentWeather.cs b/BotNet.Services/Weather/CurrentWeather.cs index 470df45..5ade73d 100644 --- a/BotNet.Services/Weather/CurrentWeather.cs +++ b/BotNet.Services/Weather/CurrentWeather.cs @@ -9,32 +9,32 @@ using Microsoft.Extensions.Options; namespace BotNet.Services.Weather { - public class CurrentWeather : Weather { - public CurrentWeather( - IOptions options, - HttpClient httpClient - ) : base(options, httpClient) { - - } - - public async Task<(string Text, string Icon)> GetCurrentWeatherAsync(string? place, CancellationToken cancellationToken) { - string url = string.Format(_uriTemplate, "current"); + public class CurrentWeather( + IOptions options, + HttpClient httpClient + ) : Weather(options, httpClient) { + public async Task<(string Text, string Icon)> GetCurrentWeatherAsync( + string? place, + CancellationToken cancellationToken + ) { + string url = string.Format(UriTemplate, "current"); if (string.IsNullOrEmpty(place)) { throw new ArgumentNullException(nameof(place)); } - if (string.IsNullOrEmpty(_apiKey)) { - throw new ArgumentNullException(nameof(_apiKey)); + if (string.IsNullOrEmpty(ApiKey)) { + throw new ArgumentNullException(nameof(ApiKey)); } - Uri uri = new(url + $"?key={_apiKey}&q={place}"); - HttpResponseMessage response = await _httpClient.GetAsync(uri.AbsoluteUri, cancellationToken); + Uri uri = new($"{url}?key={ApiKey}&q={place}"); + HttpResponseMessage response = await HttpClient.GetAsync(uri.AbsoluteUri, cancellationToken); if (response is not { StatusCode: HttpStatusCode.OK, Content.Headers.ContentType.MediaType: string contentType }) { throw new HttpRequestException("Unable to find location."); } - if (response.Content is not object && contentType is not "application/json") { + if (response.Content is not object && + contentType is not "application/json") { throw new HttpRequestException("Failed to parse result."); } @@ -46,18 +46,20 @@ HttpClient httpClient throw new JsonException("Failed to parse result."); } - string textResult = $"Cuaca {place} saat ini\n" - + $"Local Time: {weatherResponse!.Location!.LocalTime}\n" - + $"Condition: {weatherResponse!.Current!.Condition!.Text}\n" - + $"Temperature: {weatherResponse!.Current!.Temp_C} Celcius\n" - + $"Wind: {weatherResponse!.Current!.Wind_Kph} km/h"; + string textResult = $""" + Cuaca {place} saat ini + Local Time: {weatherResponse.Location!.LocalTime} + Condition: {weatherResponse.Current!.Condition!.Text} + Temperature: {weatherResponse.Current!.Temp_C} Celcius + Wind: {weatherResponse.Current!.Wind_Kph} km/h + """; - string icon = weatherResponse!.Current!.Condition!.Icon!; + string icon = weatherResponse.Current!.Condition!.Icon!; return ( Text: textResult, Icon: icon.Remove(0, 2) - ); + ); } } } diff --git a/BotNet.Services/Weather/Models/Current.cs b/BotNet.Services/Weather/Models/Current.cs index 8caeb08..db12a11 100644 --- a/BotNet.Services/Weather/Models/Current.cs +++ b/BotNet.Services/Weather/Models/Current.cs @@ -1,4 +1,5 @@ -namespace BotNet.Services.Weather.Models { +// ReSharper disable InconsistentNaming +namespace BotNet.Services.Weather.Models { public class Current { public string? Last_Updated { get; set; } public double Temp_C { get; set; } diff --git a/BotNet.Services/Weather/Weather.cs b/BotNet.Services/Weather/Weather.cs index 22927ad..bd14019 100644 --- a/BotNet.Services/Weather/Weather.cs +++ b/BotNet.Services/Weather/Weather.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using Microsoft.Extensions.Options; namespace BotNet.Services.Weather { - public class Weather { - protected readonly string? _apiKey; - protected string _uriTemplate = "https://api.weatherapi.com/v1/{0}.json"; - protected readonly HttpClient _httpClient; - - public Weather(IOptions options, HttpClient httpClient) { - _apiKey = options.Value.ApiKey; - _httpClient = httpClient; - } + public class Weather( + IOptions options, + HttpClient httpClient + ) { + protected readonly string? ApiKey = options.Value.ApiKey; + protected const string UriTemplate = "https://api.weatherapi.com/v1/{0}.json"; + protected readonly HttpClient HttpClient = httpClient; } } diff --git a/BotNet.Tests/BotNet.Tests.csproj b/BotNet.Tests/BotNet.Tests.csproj index 78ae467..f046526 100644 --- a/BotNet.Tests/BotNet.Tests.csproj +++ b/BotNet.Tests/BotNet.Tests.csproj @@ -3,7 +3,6 @@ net9.0 enable - false diff --git a/BotNet.Tests/Services/SocialLink/SocialLinkEmbedFixerTests.cs b/BotNet.Tests/Services/SocialLink/SocialLinkEmbedFixerTests.cs index 237de84..43d0b63 100644 --- a/BotNet.Tests/Services/SocialLink/SocialLinkEmbedFixerTests.cs +++ b/BotNet.Tests/Services/SocialLink/SocialLinkEmbedFixerTests.cs @@ -39,8 +39,9 @@ public void CanReplaceSocialLink(string url, string? replacedUrl) { new[] { "https://www.twitter.com/ShowwcaseHQ/status/1556259601829576707", "https://instagram.com/reel/C0XXKVnpRUI" })] [InlineData("Iyakah?", new string[] { })] public void CanDetectSocialLink(string message, IEnumerable? urls) { - List? resUrls = - SocialLinkEmbedFixer.GetPossibleUrls(message)?.Select(u => u.OriginalString).ToList(); + List resUrls = + SocialLinkEmbedFixer.GetPossibleUrls(message) + .Select(u => u.OriginalString).ToList(); if (urls == null) { resUrls.Should().BeNull(); } else { diff --git a/BotNet.Tests/TestUtilities/HttpClientMock.cs b/BotNet.Tests/TestUtilities/HttpClientMock.cs index a6d6f7f..03b019a 100644 --- a/BotNet.Tests/TestUtilities/HttpClientMock.cs +++ b/BotNet.Tests/TestUtilities/HttpClientMock.cs @@ -11,10 +11,9 @@ public class HttpClientMock { public static async Task TestHttpClientUsingDummyContentAsync(string content, Func testAsync) { Mock handlerMock = new(); - using HttpResponseMessage responseMessage = new() { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(content) - }; + using HttpResponseMessage responseMessage = new(); + responseMessage.StatusCode = HttpStatusCode.OK; + responseMessage.Content = new StringContent(content); handlerMock .Protected() diff --git a/BotNet.sln.DotSettings b/BotNet.sln.DotSettings new file mode 100644 index 0000000..da7bff1 --- /dev/null +++ b/BotNet.sln.DotSettings @@ -0,0 +1,254 @@ + + Required + Required + Required + Required + END_OF_LINE + END_OF_LINE + END_OF_LINE + END_OF_LINE + TOGETHER + True + True + END_OF_LINE + END_OF_LINE + True + 1 + 2 + END_OF_LINE + False + False + ALWAYS + ALWAYS + ALWAYS + NEVER + False + False + ALWAYS + False + False + False + END_OF_LINE + True + True + True + True + True + True + True + False + True + CHOP_ALWAYS + WRAP_IF_LONG + False + CHOP_ALWAYS + WRAP_IF_LONG + CHOP_ALWAYS + CHOP_ALWAYS + CHOP_ALWAYS + CHOP_ALWAYS + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/BotNet.sln.DotSettings.user b/BotNet.sln.DotSettings.user new file mode 100644 index 0000000..36593d7 --- /dev/null +++ b/BotNet.sln.DotSettings.user @@ -0,0 +1,7 @@ + + ForceIncluded + ShowAndRun + C:\Users\Ronny\AppData\Local\JetBrains\Rider2024.3\resharper-host\temp\Rider\vAny\CoverageData\_BotNet.653754137\Snapshot\snapshot.utdcvr + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Solution /> +</SessionState> \ No newline at end of file diff --git a/BotNet/Bot/BotService.cs b/BotNet/Bot/BotService.cs index 33307e1..a5619cc 100644 --- a/BotNet/Bot/BotService.cs +++ b/BotNet/Bot/BotService.cs @@ -14,8 +14,6 @@ public class BotService( IOptions hostingOptionsAccessor, UpdateHandler updateHandler ) : IHostedService { - private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; - private readonly UpdateHandler _updateHandler = updateHandler; private readonly string _botToken = botOptionsAccessor.Value.AccessToken!; private readonly string _hostName = hostingOptionsAccessor.Value.HostName!; private readonly bool _useLongPolling = hostingOptionsAccessor.Value.UseLongPolling; @@ -24,28 +22,28 @@ UpdateHandler updateHandler public Task StartAsync(CancellationToken cancellationToken) { _cancellationTokenSource = new(); if (_useLongPolling) { - _telegramBotClient.StartReceiving(_updateHandler, cancellationToken: _cancellationTokenSource.Token); + telegramBotClient.StartReceiving(updateHandler, cancellationToken: _cancellationTokenSource.Token); return Task.CompletedTask; - } else { - string webhookAddress = $"https://{_hostName}/webhook/{_botToken.Split(':')[1]}"; - return _telegramBotClient.SetWebhook( - url: webhookAddress, - allowedUpdates: [ - UpdateType.CallbackQuery, - UpdateType.InlineQuery, - UpdateType.Message, - ], - cancellationToken: cancellationToken - ); } + + string webhookAddress = $"https://{_hostName}/webhook/{_botToken.Split(':')[1]}"; + return telegramBotClient.SetWebhook( + url: webhookAddress, + allowedUpdates: [ + UpdateType.CallbackQuery, + UpdateType.InlineQuery, + UpdateType.Message, + ], + cancellationToken: cancellationToken + ); } public Task StopAsync(CancellationToken cancellationToken) { _cancellationTokenSource?.Cancel(); if (_useLongPolling) { - return _telegramBotClient.Close(cancellationToken); - } else { - return _telegramBotClient.DeleteWebhook(cancellationToken: cancellationToken); + return telegramBotClient.Close(cancellationToken); } + + return telegramBotClient.DeleteWebhook(cancellationToken: cancellationToken); } } diff --git a/BotNet/Bot/CommandConsumer.cs b/BotNet/Bot/CommandConsumer.cs index 4efd85f..3ae6b6d 100644 --- a/BotNet/Bot/CommandConsumer.cs +++ b/BotNet/Bot/CommandConsumer.cs @@ -12,9 +12,6 @@ internal sealed class CommandConsumer( IMediator mediator, ILogger logger ) : IHostedService { - private readonly ICommandQueue _commandQueue = commandQueue; - private readonly IMediator _mediator = mediator; - private readonly ILogger _logger = logger; private CancellationTokenSource? _cancellationTokenSource; private TaskCompletionSource? _shutdownCompletionSource; @@ -28,20 +25,20 @@ public Task StartAsync(CancellationToken cancellationToken) { while (_cancellationTokenSource is { IsCancellationRequested: false }) { // Execution strategy is defined here. // Current strategy is sequential, not concurrent, no DLQ, in single queue. - ICommand command = await _commandQueue.ReceiveAsync(_cancellationTokenSource.Token); - await _mediator.Send(command, _cancellationTokenSource.Token); + ICommand command = await commandQueue.ReceiveAsync(_cancellationTokenSource.Token); + await mediator.Send(command, _cancellationTokenSource.Token); } } catch (OperationCanceledException) { // Graceful shutdown _shutdownCompletionSource?.TrySetResult(); } catch (Exception exc) { if (_cancellationTokenSource is not { IsCancellationRequested: false }) { - _logger.LogError(exc, "Command consumer crashed."); + logger.LogError(exc, "Command consumer crashed."); _shutdownCompletionSource?.TrySetException(exc); return; } - _logger.LogError(exc, "Command consumer crashed. Restarting in 5 seconds..."); + logger.LogError(exc, "Command consumer crashed. Restarting in 5 seconds..."); try { await Task.Delay(5000, _cancellationTokenSource.Token); goto Restart; @@ -56,7 +53,10 @@ public Task StartAsync(CancellationToken cancellationToken) { } public async Task StopAsync(CancellationToken cancellationToken) { - _cancellationTokenSource?.Cancel(); + if (_cancellationTokenSource != null) { + await _cancellationTokenSource.CancelAsync(); + } + _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; if (_shutdownCompletionSource != null) { diff --git a/BotNet/Bot/UpdateHandler.cs b/BotNet/Bot/UpdateHandler.cs index 2db300c..19994b8 100644 --- a/BotNet/Bot/UpdateHandler.cs +++ b/BotNet/Bot/UpdateHandler.cs @@ -17,9 +17,6 @@ public class UpdateHandler( IMediator mediator, ILogger logger ) : IUpdateHandler { - private readonly IMediator _mediator = mediator; - private readonly ILogger _logger = logger; - public async Task HandleUpdateAsync( ITelegramBotClient botClient, Update update, @@ -28,21 +25,19 @@ CancellationToken cancellationToken try { switch (update.Type) { case UpdateType.Message: - await _mediator.Send(new MessageUpdate(update.Message!)); + await mediator.Send(new MessageUpdate(update.Message!), cancellationToken); break; case UpdateType.InlineQuery: - await _mediator.Send(new InlineQueryUpdate(update.InlineQuery!)); + await mediator.Send(new InlineQueryUpdate(update.InlineQuery!), cancellationToken); break; case UpdateType.CallbackQuery: - await _mediator.Send(new CallbackQueryUpdate(update.CallbackQuery!)); - break; - default: + await mediator.Send(new CallbackQueryUpdate(update.CallbackQuery!), cancellationToken); break; } } catch (OperationCanceledException) { throw; } catch (Exception exc) { - _logger.LogError(exc, "{message}", exc.Message); + logger.LogError(exc, "{message}", exc.Message); } } @@ -56,7 +51,7 @@ CancellationToken cancellationToken $"Telegram API Error:\n{apiRequestException.ErrorCode}\n{apiRequestException.Message}", _ => exception.ToString() }; - _logger.LogError(exception, "{message}", errorMessage); + logger.LogError(exception, "{message}", errorMessage); return Task.CompletedTask; } } diff --git a/BotNet/Program.cs b/BotNet/Program.cs index 8914412..490e56c 100644 --- a/BotNet/Program.cs +++ b/BotNet/Program.cs @@ -58,7 +58,7 @@ builder.Services.Configure(builder.Configuration.GetSection("V8Options")); builder.Services.Configure(builder.Configuration.GetSection("PistonOptions")); builder.Services.Configure(builder.Configuration.GetSection("PestoOptions")); -builder.Services.Configure(builder.Configuration.GetSection("OpenAIOptions")); +builder.Services.Configure(builder.Configuration.GetSection("OpenAIOptions")); builder.Services.Configure(builder.Configuration.GetSection("StabilityOptions")); builder.Services.Configure(builder.Configuration.GetSection("GoogleMapOptions")); builder.Services.Configure(builder.Configuration.GetSection("WeatherOptions")); @@ -73,7 +73,7 @@ builder.Services.AddV8Evaluator(); builder.Services.AddPistonClient(); builder.Services.AddPestoClient(); -builder.Services.AddOpenAIClient(); +builder.Services.AddOpenAiClient(); builder.Services.AddProgrammerHumorScraper(); builder.Services.AddTiktokServices(); builder.Services.AddCSharpEvaluator(); @@ -83,7 +83,7 @@ builder.Services.AddTokopediaServices(); builder.Services.AddGoogleMaps(); builder.Services.AddWeatherService(); -builder.Services.AddBMKG(); +builder.Services.AddBmkg(); builder.Services.AddPreviewServices(); builder.Services.AddMemeGenerator(); builder.Services.AddBubbleWrapKeyboardGenerator(); @@ -96,7 +96,7 @@ builder.Services.AddSqliteDatabases(); builder.Services.AddPemilu2024(); builder.Services.AddGoogleSheets(); -builder.Services.AddKokizzuVPSBenchmarkDataSource(); +builder.Services.AddKokizzuVpsBenchmarkDataSource(); // MediatR builder.Services.AddMediatR(config => { diff --git a/BotNet/Views/DecimalClock/DecimalClockSvgBuilder.cs b/BotNet/Views/DecimalClock/DecimalClockSvgBuilder.cs index 1ea6160..c3760bf 100644 --- a/BotNet/Views/DecimalClock/DecimalClockSvgBuilder.cs +++ b/BotNet/Views/DecimalClock/DecimalClockSvgBuilder.cs @@ -11,16 +11,16 @@ public static string GenerateSvg() { for (int i = 0; i < 10; i++) { double x = 400 - Math.Sin(Math.PI * i / 5) * 320; double y = 400 + Math.Cos(Math.PI * i / 5) * 320; - digitsBuilder.Append($$""" - {{i}} + digitsBuilder.Append($""" + {i} """); } StringBuilder ticksBuilder = new(); for (int i = 0; i < 100; i++) { - ticksBuilder.Append($$""" - + ticksBuilder.Append($""" + """); } diff --git a/BotNet/Views/DecimalClock/Svg.cshtml b/BotNet/Views/DecimalClock/Svg.cshtml index e6d5aa6..1c75ede 100644 --- a/BotNet/Views/DecimalClock/Svg.cshtml +++ b/BotNet/Views/DecimalClock/Svg.cshtml @@ -1,6 +1,6 @@ @{ - TimeSpan timeOfDay = DateTime.UtcNow.AddHours(7).TimeOfDay; - double fractionOfDay = timeOfDay / TimeSpan.FromDays(1); + var timeOfDay = DateTime.UtcNow.AddHours(7).TimeOfDay; + var fractionOfDay = timeOfDay / TimeSpan.FromDays(1); }