diff --git a/BotNet.CommandHandlers/AI/OpenAI/AskCommandHandler.cs b/BotNet.CommandHandlers/AI/OpenAI/AskCommandHandler.cs index 5a33c69..508f939 100644 --- a/BotNet.CommandHandlers/AI/OpenAI/AskCommandHandler.cs +++ b/BotNet.CommandHandlers/AI/OpenAI/AskCommandHandler.cs @@ -1,7 +1,8 @@ using BotNet.Commands; using BotNet.Commands.AI.OpenAI; using BotNet.Commands.BotUpdate.Message; -using BotNet.Commands.CommandPrioritization; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; using BotNet.Services.MarkdownV2; using BotNet.Services.OpenAI; using BotNet.Services.OpenAI.Models; @@ -23,18 +24,18 @@ ILogger logger private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; private readonly ILogger _logger = logger; - public async Task Handle(AskCommand command, CancellationToken cancellationToken) { + public async Task Handle(AskCommand askCommand, CancellationToken cancellationToken) { try { OpenAITextPromptHandler.CHAT_RATE_LIMITER.ValidateActionRate( - chatId: command.ChatId, - userId: command.SenderId + chatId: askCommand.Command.Chat.Id, + userId: askCommand.Command.Sender.Id ); } catch (RateLimitExceededException exc) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: askCommand.Command.Chat.Id, text: $"Anda terlalu banyak memanggil AI. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, - replyToMessageId: command.PromptMessageId, + replyToMessageId: askCommand.Command.MessageId, cancellationToken: cancellationToken ); return; @@ -44,30 +45,27 @@ await _telegramBotClient.SendTextMessageAsync( Task _ = Task.Run(async () => { 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", command.Prompt) + ChatMessage.FromText("user", askCommand.Prompt) ]; messages.AddRange( - from message in command.Thread.Take(10).Reverse() + from message in askCommand.Thread.Take(10).Reverse() select ChatMessage.FromText( - role: message.SenderName switch { - "AI" or "Bot" or "GPT" => "assistant", - _ => "user" - }, + role: message.Sender.ChatGPTRole, text: message.Text ) ); Message responseMessage = await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: askCommand.Command.Chat.Id, text: MarkdownV2Sanitizer.Sanitize("… ⏳"), parseMode: ParseMode.MarkdownV2, - replyToMessageId: command.PromptMessageId + replyToMessageId: askCommand.Command.MessageId ); string response = await _openAIClient.ChatAsync( - model: command.CommandPriority switch { - CommandPriority.VIPChat or CommandPriority.HomeGroupChat => "gpt-4-1106-preview", + model: askCommand switch { + ({ Command: { Sender: VIPSender } or { Chat: HomeGroupChat } }) => "gpt-4-1106-preview", _ => "gpt-3.5-turbo" }, messages: messages, @@ -78,7 +76,7 @@ select ChatMessage.FromText( // Finalize message try { responseMessage = await telegramBotClient.EditMessageTextAsync( - chatId: command.ChatId, + chatId: askCommand.Command.Chat.Id, messageId: responseMessage.MessageId, text: MarkdownV2Sanitizer.Sanitize(response), parseMode: ParseMode.MarkdownV2, @@ -93,7 +91,7 @@ select ChatMessage.FromText( _telegramMessageCache.Add( message: AIResponseMessage.FromMessage( message: responseMessage, - replyToMessageId: command.PromptMessageId, + replyToMessage: askCommand.Command, callSign: "AI" ) ); diff --git a/BotNet.CommandHandlers/AI/OpenAI/OpenAIImageGenerationPromptHandler.cs b/BotNet.CommandHandlers/AI/OpenAI/OpenAIImageGenerationPromptHandler.cs index dd530e7..9df6763 100644 --- a/BotNet.CommandHandlers/AI/OpenAI/OpenAIImageGenerationPromptHandler.cs +++ b/BotNet.CommandHandlers/AI/OpenAI/OpenAIImageGenerationPromptHandler.cs @@ -32,7 +32,7 @@ public Task Handle(OpenAIImageGenerationPrompt command, CancellationToken cancel } catch (Exception exc) { _logger.LogError(exc, "Could not generate image"); await _telegramBotClient.EditMessageTextAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, messageId: command.ResponseMessageId, text: "Failed to generate image.", parseMode: ParseMode.Html, @@ -44,7 +44,7 @@ await _telegramBotClient.EditMessageTextAsync( // Delete busy message try { await _telegramBotClient.DeleteMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, messageId: command.ResponseMessageId, cancellationToken: cancellationToken ); @@ -54,7 +54,7 @@ await _telegramBotClient.DeleteMessageAsync( // Send generated image Message responseMessage = await _telegramBotClient.SendPhotoAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, photo: new InputFileUrl(generatedImageUrl), replyToMessageId: command.PromptMessageId, cancellationToken: cancellationToken diff --git a/BotNet.CommandHandlers/AI/OpenAI/OpenAITextPromptHandler.cs b/BotNet.CommandHandlers/AI/OpenAI/OpenAITextPromptHandler.cs index 024e74b..59430e5 100644 --- a/BotNet.CommandHandlers/AI/OpenAI/OpenAITextPromptHandler.cs +++ b/BotNet.CommandHandlers/AI/OpenAI/OpenAITextPromptHandler.cs @@ -3,7 +3,8 @@ using BotNet.Commands.AI.OpenAI; using BotNet.Commands.AI.Stability; using BotNet.Commands.BotUpdate.Message; -using BotNet.Commands.CommandPrioritization; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; using BotNet.Services.MarkdownV2; using BotNet.Services.OpenAI; using BotNet.Services.OpenAI.Models; @@ -29,18 +30,18 @@ ILogger logger private readonly OpenAIClient _openAIClient = openAIClient; private readonly ILogger _logger = logger; - public Task Handle(OpenAITextPrompt command, CancellationToken cancellationToken) { + public Task Handle(OpenAITextPrompt textPrompt, CancellationToken cancellationToken) { try { CHAT_RATE_LIMITER.ValidateActionRate( - chatId: command.ChatId, - userId: command.SenderId + chatId: textPrompt.Command.Chat.Id, + userId: textPrompt.Command.Sender.Id ); } catch (RateLimitExceededException exc) { return _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: textPrompt.Command.Chat.Id, text: $"Anda terlalu banyak memanggil AI. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, - replyToMessageId: command.PromptMessageId, + replyToMessageId: textPrompt.Command.MessageId, cancellationToken: cancellationToken ); } @@ -52,30 +53,27 @@ public Task Handle(OpenAITextPrompt command, CancellationToken cancellationToken ]; messages.AddRange( - from message in command.Thread.Take(10).Reverse() + from message in textPrompt.Thread.Take(10).Reverse() select ChatMessage.FromText( - role: message.SenderName switch { - "AI" or "Bot" or "GPT" => "assistant", - _ => "user" - }, + role: message.Sender.ChatGPTRole, text: message.Text ) ); messages.Add( - ChatMessage.FromText("user", command.Prompt) + ChatMessage.FromText("user", textPrompt.Prompt) ); Message responseMessage = await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: textPrompt.Command.Chat.Id, text: MarkdownV2Sanitizer.Sanitize("… ⏳"), parseMode: ParseMode.MarkdownV2, - replyToMessageId: command.PromptMessageId + replyToMessageId: textPrompt.Command.MessageId ); string response = await _openAIClient.ChatAsync( - model: command.CommandPriority switch { - CommandPriority.VIPChat or CommandPriority.HomeGroupChat => "gpt-4-1106-preview", + model: textPrompt switch { + ({ Command: { Sender: VIPSender } or { Chat: HomeGroupChat } }) => "gpt-4-1106-preview", _ => "gpt-3.5-turbo" }, messages: messages, @@ -85,15 +83,15 @@ select ChatMessage.FromText( // Handle image generation intent if (response.StartsWith("ImageGeneration:")) { - if (command.CommandPriority != CommandPriority.VIPChat) { + if (textPrompt.Command.Sender is not VIPSender) { try { - ArtCommandHandler.IMAGE_GENERATION_RATE_LIMITER.ValidateActionRate(command.ChatId, command.SenderId); + ArtCommandHandler.IMAGE_GENERATION_RATE_LIMITER.ValidateActionRate(textPrompt.Command.Chat.Id, textPrompt.Command.Sender.Id); } catch (RateLimitExceededException exc) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: textPrompt.Command.Chat.Id, text: $"Anda belum mendapat giliran. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, - replyToMessageId: command.PromptMessageId, + replyToMessageId: textPrompt.Command.MessageId, cancellationToken: cancellationToken ); return; @@ -101,36 +99,34 @@ await _telegramBotClient.SendTextMessageAsync( } string imageGenerationPrompt = response.Substring(response.IndexOf(':') + 1).Trim(); - switch (command.CommandPriority) { - case CommandPriority.VIPChat: + switch (textPrompt.Command) { + case { Sender: VIPSender }: await _commandQueue.DispatchAsync( command: new OpenAIImageGenerationPrompt( - callSign: command.CallSign, + callSign: textPrompt.CallSign, prompt: imageGenerationPrompt, - promptMessageId: command.PromptMessageId, - responseMessageId: responseMessage.MessageId, - chatId: command.ChatId, - senderId: command.SenderId, - commandPriority: command.CommandPriority + promptMessageId: textPrompt.Command.MessageId, + responseMessageId: new(responseMessage.MessageId), + chat: textPrompt.Command.Chat, + sender: textPrompt.Command.Sender ) ); break; - case CommandPriority.HomeGroupChat: + case { Chat: HomeGroupChat }: await _commandQueue.DispatchAsync( command: new StabilityTextToImagePrompt( - callSign: command.CallSign, + callSign: textPrompt.CallSign, prompt: imageGenerationPrompt, - promptMessageId: command.PromptMessageId, - responseMessageId: responseMessage.MessageId, - chatId: command.ChatId, - senderId: command.SenderId, - commandPriority: command.CommandPriority + promptMessageId: textPrompt.Command.MessageId, + responseMessageId: new(responseMessage.MessageId), + chat: textPrompt.Command.Chat, + sender: textPrompt.Command.Sender ) ); break; default: await _telegramBotClient.EditMessageTextAsync( - chatId: command.ChatId, + chatId: textPrompt.Command.Chat.Id, messageId: responseMessage.MessageId, text: MarkdownV2Sanitizer.Sanitize("Image generation tidak bisa dipakai di sini."), parseMode: ParseMode.MarkdownV2, @@ -144,7 +140,7 @@ await _telegramBotClient.EditMessageTextAsync( // Finalize message try { responseMessage = await telegramBotClient.EditMessageTextAsync( - chatId: command.ChatId, + chatId: textPrompt.Command.Chat.Id, messageId: responseMessage.MessageId, text: MarkdownV2Sanitizer.Sanitize(response), parseMode: ParseMode.MarkdownV2, @@ -159,8 +155,8 @@ await _telegramBotClient.EditMessageTextAsync( _telegramMessageCache.Add( message: AIResponseMessage.FromMessage( message: responseMessage, - replyToMessageId: command.PromptMessageId, - callSign: command.CallSign + replyToMessage: textPrompt.Command, + callSign: textPrompt.CallSign ) ); }); diff --git a/BotNet.CommandHandlers/AI/Stability/StabilityTextToImagePromptHandler.cs b/BotNet.CommandHandlers/AI/Stability/StabilityTextToImagePromptHandler.cs index ea6cbba..64fc171 100644 --- a/BotNet.CommandHandlers/AI/Stability/StabilityTextToImagePromptHandler.cs +++ b/BotNet.CommandHandlers/AI/Stability/StabilityTextToImagePromptHandler.cs @@ -29,7 +29,7 @@ public Task Handle(StabilityTextToImagePrompt command, CancellationToken cancell ); } catch (ContentFilteredException exc) { await _telegramBotClient.EditMessageTextAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, messageId: command.ResponseMessageId, text: $"{exc.Message ?? "Content filtered."}", parseMode: ParseMode.Html, @@ -38,7 +38,7 @@ await _telegramBotClient.EditMessageTextAsync( return; } catch { await _telegramBotClient.EditMessageTextAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, messageId: command.ResponseMessageId, text: "Failed to generate image.", parseMode: ParseMode.Html, @@ -50,7 +50,7 @@ await _telegramBotClient.EditMessageTextAsync( // Delete busy message try { await _telegramBotClient.DeleteMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, messageId: command.ResponseMessageId, cancellationToken: cancellationToken ); @@ -61,7 +61,7 @@ await _telegramBotClient.DeleteMessageAsync( // Send generated image using MemoryStream generatedImageStream = new(generatedImage); Message responseMessage = await _telegramBotClient.SendPhotoAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, photo: new InputFileStream(generatedImageStream, "art.png"), replyToMessageId: command.PromptMessageId, cancellationToken: cancellationToken diff --git a/BotNet.CommandHandlers/Art/ArtCommandHandler.cs b/BotNet.CommandHandlers/Art/ArtCommandHandler.cs index 087c290..cae9481 100644 --- a/BotNet.CommandHandlers/Art/ArtCommandHandler.cs +++ b/BotNet.CommandHandlers/Art/ArtCommandHandler.cs @@ -2,7 +2,8 @@ using BotNet.Commands.AI.OpenAI; using BotNet.Commands.AI.Stability; using BotNet.Commands.Art; -using BotNet.Commands.CommandPrioritization; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; using BotNet.Services.MarkdownV2; using BotNet.Services.RateLimit; using Telegram.Bot; @@ -21,10 +22,10 @@ ICommandQueue commandQueue public Task Handle(ArtCommand command, CancellationToken cancellationToken) { try { - IMAGE_GENERATION_RATE_LIMITER.ValidateActionRate(command.ChatId, command.SenderId); + IMAGE_GENERATION_RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException exc) { return _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: $"Anda belum mendapat giliran. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, replyToMessageId: command.PromptMessageId, @@ -35,10 +36,10 @@ public Task Handle(ArtCommand command, CancellationToken cancellationToken) { // Fire and forget Task.Run(async () => { try { - switch (command.CommandPriority) { - case CommandPriority.VIPChat: { + switch (command) { + case { Sender: VIPSender }: { Message busyMessage = await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Generating image… ⏳", parseMode: ParseMode.Markdown, replyToMessageId: command.PromptMessageId, @@ -50,17 +51,16 @@ await _commandQueue.DispatchAsync( callSign: "AI", prompt: command.Prompt, promptMessageId: command.PromptMessageId, - responseMessageId: busyMessage.MessageId, - chatId: command.ChatId, - senderId: command.SenderId, - commandPriority: command.CommandPriority + responseMessageId: new(busyMessage.MessageId), + chat: command.Chat, + sender: command.Sender ) ); } break; - case CommandPriority.HomeGroupChat: { + case { Chat: HomeGroupChat }: { Message busyMessage = await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Generating image… ⏳", parseMode: ParseMode.Markdown, replyToMessageId: command.PromptMessageId, @@ -72,17 +72,16 @@ await _commandQueue.DispatchAsync( callSign: "AI", prompt: command.Prompt, promptMessageId: command.PromptMessageId, - responseMessageId: busyMessage.MessageId, - chatId: command.ChatId, - senderId: command.SenderId, - commandPriority: command.CommandPriority + responseMessageId: new(busyMessage.MessageId), + chat: command.Chat, + sender: command.Sender ) ); } break; default: await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: MarkdownV2Sanitizer.Sanitize("Image generation tidak bisa dipakai di sini."), parseMode: ParseMode.MarkdownV2, replyToMessageId: command.PromptMessageId, diff --git a/BotNet.CommandHandlers/BMKG/BMKGCommandHandler.cs b/BotNet.CommandHandlers/BMKG/BMKGCommandHandler.cs index 9c20bfe..7011f3c 100644 --- a/BotNet.CommandHandlers/BMKG/BMKGCommandHandler.cs +++ b/BotNet.CommandHandlers/BMKG/BMKGCommandHandler.cs @@ -17,10 +17,10 @@ LatestEarthQuake latestEarthQuake public Task Handle(BMKGCommand command, CancellationToken cancellationToken) { try { - RATE_LIMITER.ValidateActionRate(command.ChatId, command.SenderId); + RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException exc) { return _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: $"Sabar dulu ya, tunggu giliran yang lain. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, replyToMessageId: command.CommandMessageId, @@ -34,7 +34,7 @@ public Task Handle(BMKGCommand command, CancellationToken cancellationToken) { (string text, string shakemapUrl) = await _latestEarthQuake.GetLatestAsync(); await _telegramBotClient.SendPhotoAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, photo: new InputFileUrl(shakemapUrl), caption: text, replyToMessageId: command.CommandMessageId, diff --git a/BotNet.CommandHandlers/BotUpdate/Message/AICallCommandHandler.cs b/BotNet.CommandHandlers/BotUpdate/Message/AICallCommandHandler.cs index c6b33b4..56926fb 100644 --- a/BotNet.CommandHandlers/BotUpdate/Message/AICallCommandHandler.cs +++ b/BotNet.CommandHandlers/BotUpdate/Message/AICallCommandHandler.cs @@ -19,16 +19,14 @@ public async Task Handle(AICallCommand command, CancellationToken cancellationTo await _commandQueue.DispatchAsync( command: OpenAITextPrompt.FromAICallCommand( aiCallCommand: command, - thread: command.ReplyToMessageId.HasValue - ? _telegramMessageCache.GetThread( - messageId: command.ReplyToMessageId.Value, - chatId: command.ChatId - ) + thread: command.ReplyToMessage is { } replyToMessage + ? _telegramMessageCache.GetThread(replyToMessage) : Enumerable.Empty() ) ); break; case "AI" or "Bot" or "GPT" when command.ImageFileId is { } imageFileId: + // TODO: Implement GPT-4 Vision break; } } diff --git a/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs b/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs index 20b5745..e837ab0 100644 --- a/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs +++ b/BotNet.CommandHandlers/BotUpdate/Message/MessageUpdateHandler.cs @@ -1,6 +1,5 @@ using BotNet.Commands; using BotNet.Commands.BotUpdate.Message; -using BotNet.Commands.CommandPrioritization; using BotNet.Services.BotProfile; using BotNet.Services.SocialLink; using RG.Ninja; @@ -12,13 +11,11 @@ public sealed class MessageUpdateHandler( ITelegramBotClient telegramBotClient, ICommandQueue commandQueue, ITelegramMessageCache telegramMessageCache, - CommandPriorityCategorizer commandPriorityCategorizer, BotProfileAccessor botProfileAccessor ) : ICommandHandler { private readonly ITelegramBotClient _telegramBotClient = telegramBotClient; private readonly ICommandQueue _commandQueue = commandQueue; private readonly ITelegramMessageCache _telegramMessageCache = telegramMessageCache; - private readonly CommandPriorityCategorizer _commandPriorityCategorizer = commandPriorityCategorizer; private readonly BotProfileAccessor _botProfileAccessor = botProfileAccessor; public async Task Handle(MessageUpdate update, CancellationToken cancellationToken) { @@ -30,9 +27,6 @@ public async Task Handle(MessageUpdate update, CancellationToken cancellationTok if (SlashCommand.TryCreate( message: update.Message, botUsername: (await _botProfileAccessor.GetBotProfileAsync(cancellationToken)).Username!, - commandPriority: _commandPriorityCategorizer.Categorize( - message: update.Message - ), out SlashCommand? slashCommand )) { await _commandQueue.DispatchAsync( @@ -114,9 +108,6 @@ await _telegramBotClient.SendTextMessageAsync( // Handle AI calls if (AICallCommand.TryCreate( message: update.Message!, - commandPriority: _commandPriorityCategorizer.Categorize( - message: update.Message! - ), out AICallCommand? aiCallCommand )) { // Cache both message and reply to message @@ -140,17 +131,14 @@ await _commandQueue.DispatchAsync( ReplyToMessage.MessageId: int replyToMessageId, Chat.Id: long chatId } && _telegramMessageCache.GetOrDefault( - messageId: replyToMessageId, - chatId: chatId + messageId: new(replyToMessageId), + chatId: new(chatId) ) is AIResponseMessage) { if (!AIFollowUpMessage.TryCreate( message: update.Message, - commandPriority: _commandPriorityCategorizer.Categorize( - message: update.Message - ), thread: _telegramMessageCache.GetThread( - messageId: replyToMessageId, - chatId: chatId + messageId: new(replyToMessageId), + chatId: new(chatId) ), out AIFollowUpMessage? aiFollowUpMessage )) { diff --git a/BotNet.CommandHandlers/BotUpdate/Message/SlashCommandHandler.cs b/BotNet.CommandHandlers/BotUpdate/Message/SlashCommandHandler.cs index 4f76ff1..d000558 100644 --- a/BotNet.CommandHandlers/BotUpdate/Message/SlashCommandHandler.cs +++ b/BotNet.CommandHandlers/BotUpdate/Message/SlashCommandHandler.cs @@ -109,7 +109,7 @@ await _commandQueue.DispatchAsync( } } catch (UsageException exc) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: exc.Message, parseMode: exc.ParseMode, replyToMessageId: exc.CommandMessageId, diff --git a/BotNet.CommandHandlers/Eval/EvalCommandHandler.cs b/BotNet.CommandHandlers/Eval/EvalCommandHandler.cs index a323c3e..f1a928c 100644 --- a/BotNet.CommandHandlers/Eval/EvalCommandHandler.cs +++ b/BotNet.CommandHandlers/Eval/EvalCommandHandler.cs @@ -30,7 +30,7 @@ public async Task Handle(EvalCommand command, CancellationToken cancellationToke ); } catch (ScriptEngineException exc) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "" + WebUtility.HtmlEncode(exc.Message) + "", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -39,7 +39,7 @@ await _telegramBotClient.SendTextMessageAsync( return; } catch (OperationCanceledException) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Timeout exceeded.", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -48,7 +48,7 @@ await _telegramBotClient.SendTextMessageAsync( return; } catch (JsonException exc) when (exc.Message.Contains("A possible object cycle was detected.")) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "A possible object cycle was detected.", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -67,7 +67,7 @@ await _telegramBotClient.SendTextMessageAsync( result = JsonSerializer.Serialize(resultObject, JSON_SERIALIZER_OPTIONS); } catch (Exception exc) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "" + WebUtility.HtmlEncode(exc.Message) + "", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -82,7 +82,7 @@ await _telegramBotClient.SendTextMessageAsync( if (result.Length > 1000) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Result is too long.", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -90,7 +90,7 @@ await _telegramBotClient.SendTextMessageAsync( ); } else { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + 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"))}" : $"Expression:\n{WebUtility.HtmlEncode(command.Code)}\n\nResult:\n{WebUtility.HtmlEncode(result)}", diff --git a/BotNet.CommandHandlers/Exec/ExecCommandHandler.cs b/BotNet.CommandHandlers/Exec/ExecCommandHandler.cs index dc1555b..b69e6e3 100644 --- a/BotNet.CommandHandlers/Exec/ExecCommandHandler.cs +++ b/BotNet.CommandHandlers/Exec/ExecCommandHandler.cs @@ -29,7 +29,7 @@ public Task Handle(ExecCommand command, CancellationToken cancellationToken) { if (result.Compile is { Code: not 0 }) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: $"{WebUtility.HtmlEncode(result.Compile.Stderr)}", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -37,7 +37,7 @@ await _telegramBotClient.SendTextMessageAsync( ); } else if (result.Run.Code != 0) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: $"{WebUtility.HtmlEncode(result.Run.Stderr)}", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -45,7 +45,7 @@ await _telegramBotClient.SendTextMessageAsync( ); } else if (result.Run.Output.Length > 1000 || result.Run.Output.Count(c => c == '\n') > 20) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Output is too long.", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -53,7 +53,7 @@ await _telegramBotClient.SendTextMessageAsync( ); } else { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + 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, replyToMessageId: command.CodeMessageId, @@ -64,7 +64,7 @@ await _telegramBotClient.SendTextMessageAsync( } catch (ExecutionEngineException exc) { #pragma warning restore CS0618 // Type or member is obsolete await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "" + WebUtility.HtmlEncode(exc.Message ?? "Unknown error") + "", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -72,7 +72,7 @@ await _telegramBotClient.SendTextMessageAsync( ); } catch (OperationCanceledException) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Timeout exceeded.", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, diff --git a/BotNet.CommandHandlers/FlipFlop/FlipFlopCommandHandler.cs b/BotNet.CommandHandlers/FlipFlop/FlipFlopCommandHandler.cs index 23332c9..ffe97b6 100644 --- a/BotNet.CommandHandlers/FlipFlop/FlipFlopCommandHandler.cs +++ b/BotNet.CommandHandlers/FlipFlop/FlipFlopCommandHandler.cs @@ -41,7 +41,7 @@ public async Task Handle(FlipFlopCommand command, CancellationToken cancellation // Send result image using MemoryStream resultImageStream = new(resultImage); await _telegramBotClient.SendPhotoAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, photo: new InputFileStream(resultImageStream, new string(fileInfo.FileId.Reverse().ToArray()) + ".png"), replyToMessageId: command.ImageMessageId, cancellationToken: cancellationToken diff --git a/BotNet.CommandHandlers/Fuck/FuckCommandHandler.cs b/BotNet.CommandHandlers/Fuck/FuckCommandHandler.cs index 42ce2e9..af7f6fb 100644 --- a/BotNet.CommandHandlers/Fuck/FuckCommandHandler.cs +++ b/BotNet.CommandHandlers/Fuck/FuckCommandHandler.cs @@ -16,7 +16,7 @@ public async Task Handle(FuckCommand command, CancellationToken cancellationToke code: command.Code ); await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: WebUtility.HtmlEncode(stdout), parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -24,7 +24,7 @@ await _telegramBotClient.SendTextMessageAsync( ); } catch (InvalidProgramException exc) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "" + WebUtility.HtmlEncode(exc.Message) + "", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -32,7 +32,7 @@ await _telegramBotClient.SendTextMessageAsync( ); } catch (IndexOutOfRangeException) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Memory access violation", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, @@ -40,7 +40,7 @@ await _telegramBotClient.SendTextMessageAsync( ); } catch (TimeoutException) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Operation timed out", parseMode: ParseMode.Html, replyToMessageId: command.CodeMessageId, diff --git a/BotNet.CommandHandlers/GoogleMaps/MapCommandHandler.cs b/BotNet.CommandHandlers/GoogleMaps/MapCommandHandler.cs index 9b693d2..612a48e 100644 --- a/BotNet.CommandHandlers/GoogleMaps/MapCommandHandler.cs +++ b/BotNet.CommandHandlers/GoogleMaps/MapCommandHandler.cs @@ -22,10 +22,10 @@ ILogger logger public Task Handle(MapCommand command, CancellationToken cancellationToken) { try { - SEARCH_PLACE_RATE_LIMITER.ValidateActionRate(command.ChatId, command.SenderId); + SEARCH_PLACE_RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException exc) { return _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: $"Anda belum mendapat giliran. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, replyToMessageId: command.CommandMessageId, @@ -40,7 +40,7 @@ public Task Handle(MapCommand command, CancellationToken cancellationToken) { string staticMapUrl = _staticMap.SearchPlace(command.PlaceName); await _telegramBotClient.SendPhotoAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, photo: new InputFileUrl(staticMapUrl), caption: $"View in 🗺️ Google Maps", parseMode: ParseMode.Html, @@ -52,7 +52,7 @@ await _telegramBotClient.SendPhotoAsync( } catch (Exception exc) { _logger.LogError(exc, "Could not find place"); await telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Lokasi tidak dapat ditemukan", parseMode: ParseMode.Html, replyToMessageId: command.CommandMessageId, diff --git a/BotNet.CommandHandlers/Humor/HumorCommandHandler.cs b/BotNet.CommandHandlers/Humor/HumorCommandHandler.cs index d7eebee..de853db 100644 --- a/BotNet.CommandHandlers/Humor/HumorCommandHandler.cs +++ b/BotNet.CommandHandlers/Humor/HumorCommandHandler.cs @@ -17,10 +17,10 @@ ProgrammerHumorScraper programmerHumorScraper public Task Handle(HumorCommand command, CancellationToken cancellationToken) { try { - RATE_LIMITER.ValidateActionRate(command.ChatId, command.SenderId); + RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException exc) { return _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: $"Bentar ya saya mikir dulu jokenya. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, replyToMessageId: command.CommandMessageId, @@ -35,7 +35,7 @@ public Task Handle(HumorCommand command, CancellationToken cancellationToken) { using MemoryStream imageStream = new(image); await _telegramBotClient.SendPhotoAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, photo: new InputFileStream(imageStream, "joke.webp"), caption: title, cancellationToken: cancellationToken diff --git a/BotNet.CommandHandlers/Pop/PopCommandHandler.cs b/BotNet.CommandHandlers/Pop/PopCommandHandler.cs index 15a138a..fa5fd39 100644 --- a/BotNet.CommandHandlers/Pop/PopCommandHandler.cs +++ b/BotNet.CommandHandlers/Pop/PopCommandHandler.cs @@ -11,7 +11,7 @@ ITelegramBotClient telegramBotClient public async Task Handle(PopCommand command, CancellationToken cancellationToken) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Here's a bubble wrap. Enjoy!", parseMode: ParseMode.Html, replyMarkup: BubbleWrapKeyboardGenerator.EMPTY_KEYBOARD, diff --git a/BotNet.CommandHandlers/Primbon/PrimbonCommandHandler.cs b/BotNet.CommandHandlers/Primbon/PrimbonCommandHandler.cs index b39ecbc..62f0463 100644 --- a/BotNet.CommandHandlers/Primbon/PrimbonCommandHandler.cs +++ b/BotNet.CommandHandlers/Primbon/PrimbonCommandHandler.cs @@ -18,7 +18,7 @@ ChineseCalendarScraper chineseCalendarScraper public async Task Handle(PrimbonCommand command, CancellationToken cancellationToken) { try { - RATE_LIMITER.ValidateActionRate(command.ChatId, command.SenderId); + RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); (string javaneseDate, string sangar, string restriction) = await _primbonScraper.GetTaliwangkeAsync( date: command.Date, @@ -42,7 +42,7 @@ string[] inauspiciousActivities ); await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: $$""" {{javaneseDate}} @@ -66,7 +66,7 @@ await _telegramBotClient.SendTextMessageAsync( ); } catch (RateLimitExceededException exc) when (exc is { Cooldown: var cooldown }) { await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: $"Coba lagi {cooldown}.", parseMode: ParseMode.Html, replyToMessageId: command.CommandMessageId, diff --git a/BotNet.CommandHandlers/Privilege/PrivilegeCommandHandler.cs b/BotNet.CommandHandlers/Privilege/PrivilegeCommandHandler.cs index e9febf6..9195f51 100644 --- a/BotNet.CommandHandlers/Privilege/PrivilegeCommandHandler.cs +++ b/BotNet.CommandHandlers/Privilege/PrivilegeCommandHandler.cs @@ -1,5 +1,7 @@ -using BotNet.Commands.CommandPrioritization; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.CommandPrioritization; using BotNet.Commands.Privilege; +using BotNet.Commands.SenderAggregate; using BotNet.Services.RateLimit; using Telegram.Bot; using Telegram.Bot.Types.Enums; @@ -16,7 +18,7 @@ CommandPriorityCategorizer commandPriorityCategorizer public Task Handle(PrivilegeCommand command, CancellationToken cancellationToken) { try { - RATE_LIMITER.ValidateActionRate(command.ChatId, command.SenderId); + RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException) { // Silently reject commands after rate limit exceeded return Task.CompletedTask; @@ -25,44 +27,40 @@ public Task Handle(PrivilegeCommand command, CancellationToken cancellationToken // Fire and forget Task.Run(async () => { try { - switch (command.ChatType) { - case ChatType.Private: - if (command.CommandPriority == CommandPriority.VIPChat) { - await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, - text: $$""" - 👑 Anda adalah user VIP (ID: {{command.SenderId}}) + switch (command) { + case { Chat: PrivateChat, Sender: VIPSender }: + await _telegramBotClient.SendTextMessageAsync( + chatId: command.Chat.Id, + text: $$""" + 👑 Anda adalah user VIP (ID: {{command.Sender.Id}}) 👑 GPT-4 tersedia 👑 GPT-4 Vision tersedia 👑 DALL-E 3 tersedia """, - replyToMessageId: command.CommandMessageId, - parseMode: ParseMode.Markdown, - cancellationToken: cancellationToken - ); - } else { - await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, - text: $$""" - ❌ Feature bot dibatasi di dalam private chat (ID: {{command.SenderId}}) + replyToMessageId: command.CommandMessageId, + parseMode: ParseMode.Markdown, + cancellationToken: cancellationToken + ); + break; + case { Chat: PrivateChat }: + await _telegramBotClient.SendTextMessageAsync( + chatId: command.Chat.Id, + text: $$""" + ❌ Feature bot dibatasi di dalam private chat (ID: {{command.Sender.Id}}) ✅ GPT-3.5 tersedia ❌ Vision tidak tersedia ❌ Image generation tidak tersedia """, - replyToMessageId: command.CommandMessageId, - parseMode: ParseMode.Markdown, - cancellationToken: cancellationToken - ); - } + replyToMessageId: command.CommandMessageId, + parseMode: ParseMode.Markdown, + cancellationToken: cancellationToken + ); break; - case ChatType.Group: - case ChatType.Supergroup: - if (command.CommandPriority == CommandPriority.VIPChat) { - if (_commandPriorityCategorizer.IsHomeGroup(command.ChatId)) { - await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, - text: $$""" - 👑 Group {{command.ChatTitle}} (ID: {{command.ChatId}}) adalah home group + case { Chat: HomeGroupChat, Sender: VIPSender }: + await _telegramBotClient.SendTextMessageAsync( + chatId: command.Chat.Id, + text: $$""" + 👑 Group {{command.Chat.Title}} (ID: {{command.Chat.Id}}) adalah home group 👑 GPT-4 tersedia 👑 GPT-4 Vision tersedia ✅ SDXL tersedia @@ -70,15 +68,16 @@ await _telegramBotClient.SendTextMessageAsync( 👑 Anda adalah user VIP 👑 DALL-E 3 tersedia untuk Anda """, - replyToMessageId: command.CommandMessageId, - parseMode: ParseMode.Markdown, - cancellationToken: cancellationToken - ); - } else { - await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, - text: $$""" - ⚠️ Bot dipakai di group selain home group (ID: {{command.ChatId}}) + replyToMessageId: command.CommandMessageId, + parseMode: ParseMode.Markdown, + cancellationToken: cancellationToken + ); + break; + case { Chat: GroupChat, Sender: VIPSender }: + await _telegramBotClient.SendTextMessageAsync( + chatId: command.Chat.Id, + text: $$""" + ⚠️ Bot dipakai di group selain home group (ID: {{command.Chat.Id}}) ✅ GPT-3.5 tersedia ❌ Vision tidak tersedia ❌ Image generation tidak tersedia @@ -88,38 +87,38 @@ await _telegramBotClient.SendTextMessageAsync( 👑 GPT-4 Vision tersedia untuk Anda 👑 DALL-E 3 tersedia untuk Anda """, - replyToMessageId: command.CommandMessageId, - parseMode: ParseMode.Markdown, - cancellationToken: cancellationToken - ); - } - } else if (command.CommandPriority == CommandPriority.HomeGroupChat) { - await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, - text: $$""" - 👑 Group {{command.ChatTitle}} (ID: {{command.ChatId}}) adalah home group + replyToMessageId: command.CommandMessageId, + parseMode: ParseMode.Markdown, + cancellationToken: cancellationToken + ); + break; + case { Chat: HomeGroupChat }: + await _telegramBotClient.SendTextMessageAsync( + chatId: command.Chat.Id, + text: $$""" + 👑 Group {{command.Chat.Title}} (ID: {{command.Chat.Id}}) adalah home group 👑 GPT-4 tersedia 👑 GPT-4 Vision tersedia ✅ SDXL tersedia """, - replyToMessageId: command.CommandMessageId, - parseMode: ParseMode.Markdown, - cancellationToken: cancellationToken - ); - } else { - await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, - text: $$""" - ⚠️ Bot dipakai di group selain home group (ID: {{command.ChatId}}) + replyToMessageId: command.CommandMessageId, + parseMode: ParseMode.Markdown, + cancellationToken: cancellationToken + ); + break; + case { Chat: GroupChat }: + await _telegramBotClient.SendTextMessageAsync( + chatId: command.Chat.Id, + text: $$""" + ⚠️ Bot dipakai di group selain home group (ID: {{command.Chat.Id}}) ✅ GPT-3.5 tersedia ❌ Vision tidak tersedia ❌ Image generation tidak tersedia """, - replyToMessageId: command.CommandMessageId, - parseMode: ParseMode.Markdown, - cancellationToken: cancellationToken - ); - } + replyToMessageId: command.CommandMessageId, + parseMode: ParseMode.Markdown, + cancellationToken: cancellationToken + ); break; } } catch (OperationCanceledException) { diff --git a/BotNet.CommandHandlers/TelegramMessageCache.cs b/BotNet.CommandHandlers/TelegramMessageCache.cs index f874e4d..8eecf39 100644 --- a/BotNet.CommandHandlers/TelegramMessageCache.cs +++ b/BotNet.CommandHandlers/TelegramMessageCache.cs @@ -1,5 +1,6 @@ using BotNet.Commands; using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; using Microsoft.Extensions.Caching.Memory; namespace BotNet.CommandHandlers { @@ -11,13 +12,13 @@ IMemoryCache memoryCache public void Add(MessageBase message) { _memoryCache.Set( - key: new Key(message.MessageId, message.ChatId), + key: new Key(message.MessageId, message.Chat.Id), value: message, absoluteExpirationRelativeToNow: CACHE_TTL ); } - public MessageBase? GetOrDefault(int messageId, long chatId) { + public MessageBase? GetOrDefault(MessageId messageId, ChatId chatId) { if (_memoryCache.TryGetValue( key: new Key(messageId, chatId), value: out MessageBase? message @@ -28,29 +29,29 @@ public void Add(MessageBase message) { } } - public IEnumerable GetThread(int messageId, long chatId) { + public IEnumerable GetThread(MessageId messageId, ChatId chatId) { while (GetOrDefault(messageId, chatId) is MessageBase message) { yield return message; - if (message.ReplyToMessageId == null) { + if (message.ReplyToMessage == null) { yield break; } - messageId = message.ReplyToMessageId.Value; + messageId = message.ReplyToMessage.MessageId; } } public IEnumerable GetThread(MessageBase firstMessage) { yield return firstMessage; Add(firstMessage); - if (firstMessage.ReplyToMessageId.HasValue) { - foreach (MessageBase reply in GetThread(firstMessage.ReplyToMessageId.Value, firstMessage.ChatId)) { + if (firstMessage.ReplyToMessage is not null) { + foreach (MessageBase reply in GetThread(firstMessage.ReplyToMessage.MessageId, firstMessage.Chat.Id)) { yield return reply; } } } readonly record struct Key( - int MessageId, - long ChatId + MessageId MessageId, + ChatId ChatId ); } } diff --git a/BotNet.CommandHandlers/Weather/WeatherCommandHandler.cs b/BotNet.CommandHandlers/Weather/WeatherCommandHandler.cs index fc225f3..94144a8 100644 --- a/BotNet.CommandHandlers/Weather/WeatherCommandHandler.cs +++ b/BotNet.CommandHandlers/Weather/WeatherCommandHandler.cs @@ -20,10 +20,10 @@ ILogger logger public Task Handle(WeatherCommand command, CancellationToken cancellationToken) { try { - GET_WEATHER_RATE_LIMITER.ValidateActionRate(command.ChatId, command.SenderId); + GET_WEATHER_RATE_LIMITER.ValidateActionRate(command.Chat.Id, command.Sender.Id); } catch (RateLimitExceededException exc) { return _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: $"Anda belum mendapat giliran. Coba lagi {exc.Cooldown}.", parseMode: ParseMode.Html, replyToMessageId: command.CommandMessageId, @@ -40,7 +40,7 @@ public Task Handle(WeatherCommand command, CancellationToken cancellationToken) ); await _telegramBotClient.SendPhotoAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, photo: new InputFileUrl(icon), caption: title, parseMode: ParseMode.Html, @@ -52,7 +52,7 @@ await _telegramBotClient.SendPhotoAsync( } catch (Exception exc) { _logger.LogError(exc, "Could not get weather"); await _telegramBotClient.SendTextMessageAsync( - chatId: command.ChatId, + chatId: command.Chat.Id, text: "Lokasi tidak dapat ditemukan", parseMode: ParseMode.Html, replyToMessageId: command.CommandMessageId, diff --git a/BotNet.Commands/AI/OpenAI/AskCommand.cs b/BotNet.Commands/AI/OpenAI/AskCommand.cs index 9bd48c9..04551c6 100644 --- a/BotNet.Commands/AI/OpenAI/AskCommand.cs +++ b/BotNet.Commands/AI/OpenAI/AskCommand.cs @@ -1,28 +1,18 @@ using BotNet.Commands.BotUpdate.Message; -using BotNet.Commands.CommandPrioritization; namespace BotNet.Commands.AI.OpenAI { public sealed record AskCommand : ICommand { public string Prompt { get; } - public int PromptMessageId { get; } - public long ChatId { get; } - public long SenderId { get; } - public CommandPriority CommandPriority { get; } + public SlashCommand Command { get; } public IEnumerable Thread { get; } private AskCommand( string prompt, - int promptMessageId, - long chatId, - long senderId, - CommandPriority commandPriority, + SlashCommand command, IEnumerable thread ) { Prompt = prompt; - PromptMessageId = promptMessageId; - ChatId = chatId; - SenderId = senderId; - CommandPriority = commandPriority; + Command = command; Thread = thread; } @@ -35,20 +25,17 @@ public static AskCommand FromSlashCommand(SlashCommand command, IEnumerable Thread { get; } private OpenAITextPrompt( string callSign, string prompt, - int promptMessageId, - long chatId, - long senderId, - CommandPriority commandPriority, + HumanMessageBase command, IEnumerable thread ) { CallSign = callSign; Prompt = prompt; - PromptMessageId = promptMessageId; - ChatId = chatId; - SenderId = senderId; - CommandPriority = commandPriority; + Command = command; Thread = thread; } @@ -43,10 +33,10 @@ public static OpenAITextPrompt FromAICallCommand(AICallCommand aiCallCommand, IE // Non-empty thread must begin with reply to message if (thread.FirstOrDefault() is { MessageId: { } firstMessageId, - ChatId: { } firstChatId + Chat.Id: { } firstChatId }) { - if (firstMessageId != aiCallCommand.ReplyToMessageId - || firstChatId != aiCallCommand.ChatId) { + if (firstMessageId != aiCallCommand.ReplyToMessage?.MessageId + || firstChatId != aiCallCommand.Chat.Id) { throw new ArgumentException("Thread must begin with reply to message.", nameof(thread)); } } @@ -54,10 +44,7 @@ public static OpenAITextPrompt FromAICallCommand(AICallCommand aiCallCommand, IE return new( callSign: aiCallCommand.CallSign, prompt: aiCallCommand.Text, - promptMessageId: aiCallCommand.MessageId, - chatId: aiCallCommand.ChatId, - senderId: aiCallCommand.SenderId, - commandPriority: aiCallCommand.CommandPriority, + command: aiCallCommand, thread: thread ); } @@ -76,10 +63,10 @@ public static OpenAITextPrompt FromAIFollowUpMessage(AIFollowUpMessage aiFollowU // Non-empty thread must begin with reply to message if (thread.FirstOrDefault() is { MessageId: { } firstMessageId, - ChatId: { } firstChatId + Chat.Id: { } firstChatId }) { - if (firstMessageId != aiFollowUpMessage.ReplyToMessageId - || firstChatId != aiFollowUpMessage.ChatId) { + if (firstMessageId != aiFollowUpMessage.ReplyToMessage.MessageId + || firstChatId != aiFollowUpMessage.Chat.Id) { throw new ArgumentException("Thread must begin with reply to message.", nameof(thread)); } } @@ -87,10 +74,7 @@ public static OpenAITextPrompt FromAIFollowUpMessage(AIFollowUpMessage aiFollowU return new( callSign: aiFollowUpMessage.CallSign, prompt: aiFollowUpMessage.Text, - promptMessageId: aiFollowUpMessage.MessageId, - chatId: aiFollowUpMessage.ChatId, - senderId: aiFollowUpMessage.SenderId, - commandPriority: aiFollowUpMessage.CommandPriority, + command: aiFollowUpMessage, thread: thread ); } diff --git a/BotNet.Commands/AI/Stability/StabilityTextToImagePrompt.cs b/BotNet.Commands/AI/Stability/StabilityTextToImagePrompt.cs index 75e9358..519e964 100644 --- a/BotNet.Commands/AI/Stability/StabilityTextToImagePrompt.cs +++ b/BotNet.Commands/AI/Stability/StabilityTextToImagePrompt.cs @@ -1,23 +1,23 @@ -using BotNet.Commands.CommandPrioritization; +using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.AI.Stability { public sealed record StabilityTextToImagePrompt : ICommand { public string CallSign { get; } public string Prompt { get; } - public int PromptMessageId { get; } - public int ResponseMessageId { get; } - public long ChatId { get; } - public long SenderId { get; } - public CommandPriority CommandPriority { get; } + public MessageId PromptMessageId { get; } + public MessageId ResponseMessageId { get; } + public ChatBase Chat { get; } + public HumanSender Sender { get; } public StabilityTextToImagePrompt( string callSign, string prompt, - int promptMessageId, - int responseMessageId, - long chatId, - long senderId, - CommandPriority commandPriority + MessageId promptMessageId, + MessageId responseMessageId, + ChatBase chat, + HumanSender sender ) { if (string.IsNullOrWhiteSpace(prompt)) throw new ArgumentException($"'{nameof(prompt)}' cannot be null or whitespace.", nameof(prompt)); @@ -25,9 +25,8 @@ CommandPriority commandPriority Prompt = prompt; PromptMessageId = promptMessageId; ResponseMessageId = responseMessageId; - ChatId = chatId; - SenderId = senderId; - CommandPriority = commandPriority; + Chat = chat; + Sender = sender; } } } diff --git a/BotNet.Commands/Art/ArtCommand.cs b/BotNet.Commands/Art/ArtCommand.cs index a81bbd6..9516ccb 100644 --- a/BotNet.Commands/Art/ArtCommand.cs +++ b/BotNet.Commands/Art/ArtCommand.cs @@ -1,28 +1,26 @@ using BotNet.Commands.BotUpdate.Message; -using BotNet.Commands.CommandPrioritization; +using BotNet.Commands.ChatAggregate; using BotNet.Commands.Common; +using BotNet.Commands.SenderAggregate; using Telegram.Bot.Types.Enums; namespace BotNet.Commands.Art { public sealed record ArtCommand : ICommand { public string Prompt { get; } - public int PromptMessageId { get; } - public long ChatId { get; } - public long SenderId { get; } - public CommandPriority CommandPriority { get; } + public MessageId PromptMessageId { get; } + public ChatBase Chat { get; } + public HumanSender Sender { get; } private ArtCommand( string prompt, - int promptMessageId, - long chatId, - long senderId, - CommandPriority commandPriority + MessageId promptMessageId, + ChatBase chat, + HumanSender sender ) { Prompt = prompt; PromptMessageId = promptMessageId; - ChatId = chatId; - SenderId = senderId; - CommandPriority = commandPriority; + Chat = chat; + Sender = sender; } public static ArtCommand FromSlashCommand(SlashCommand slashCommand) { @@ -43,9 +41,8 @@ public static ArtCommand FromSlashCommand(SlashCommand slashCommand) { return new( prompt: slashCommand.Text, promptMessageId: slashCommand.MessageId, - chatId: slashCommand.ChatId, - senderId: slashCommand.SenderId, - commandPriority: slashCommand.CommandPriority + chat: slashCommand.Chat, + sender: slashCommand.Sender ); } } diff --git a/BotNet.Commands/BMKG/BMKGCommand.cs b/BotNet.Commands/BMKG/BMKGCommand.cs index cadd606..5ea9b03 100644 --- a/BotNet.Commands/BMKG/BMKGCommand.cs +++ b/BotNet.Commands/BMKG/BMKGCommand.cs @@ -1,30 +1,33 @@ using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.BMKG { public sealed record BMKGCommand : ICommand { - public long ChatId { get; } - public int CommandMessageId { get; } - public long SenderId { get; } + public MessageId CommandMessageId { get; } + public ChatBase Chat { get; } + public HumanSender Sender { get; } private BMKGCommand( - long chatId, - int commandMessageId, - long senderId + MessageId commandMessageId, + ChatBase chat, + HumanSender sender ) { - ChatId = chatId; CommandMessageId = commandMessageId; - SenderId = senderId; + Chat = chat; + Sender = sender; } public static BMKGCommand FromSlashCommand(SlashCommand slashCommand) { + // Must be /bmkg if (slashCommand.Command != "/bmkg") { throw new ArgumentException("Command must be /bmkg.", nameof(slashCommand)); } return new( - chatId: slashCommand.ChatId, commandMessageId: slashCommand.MessageId, - senderId: slashCommand.SenderId + chat: slashCommand.Chat, + sender: slashCommand.Sender ); } } diff --git a/BotNet.Commands/BotUpdate/Message/AICallCommand.cs b/BotNet.Commands/BotUpdate/Message/AICallCommand.cs index d63f2d0..7824f49 100644 --- a/BotNet.Commands/BotUpdate/Message/AICallCommand.cs +++ b/BotNet.Commands/BotUpdate/Message/AICallCommand.cs @@ -1,10 +1,10 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using BotNet.Commands.CommandPrioritization; -using Telegram.Bot.Types.Enums; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.BotUpdate.Message { - public sealed record AICallCommand : MessageBase, ICommand { + public sealed record AICallCommand : HumanMessageBase, ICommand { public static readonly ImmutableHashSet CALL_SIGNS = [ "AI", "Bot", @@ -16,29 +16,19 @@ public sealed record AICallCommand : MessageBase, ICommand { public string CallSign { get; } private AICallCommand( - int messageId, - long chatId, - ChatType chatType, - string? chatTitle, - long senderId, - string senderName, - CommandPriority commandPriority, + MessageId messageId, + ChatBase chat, + HumanSender sender, string text, string? imageFileId, - int? replyToMessageId, MessageBase? replyToMessage, string callSign ) : base( messageId: messageId, - chatId: chatId, - chatType: chatType, - chatTitle: chatTitle, - senderId: senderId, - senderName: senderName, - commandPriority: commandPriority, + chat: chat, + sender: sender, text: text, imageFileId: imageFileId, - replyToMessageId: replyToMessageId, replyToMessage: replyToMessage ) { CallSign = callSign; @@ -46,48 +36,40 @@ string callSign public static bool TryCreate( Telegram.Bot.Types.Message message, - CommandPriority commandPriority, [NotNullWhen(true)] out AICallCommand? aiCallCommand ) { - // Message must contain text or caption - if ((message.Text ?? message.Caption) is not { } text) { + // Chat must be private or group + if (!ChatBase.TryCreate(message.Chat, out ChatBase? chat)) { aiCallCommand = null; return false; } - // Message must start with call sign - if (CALL_SIGNS.FirstOrDefault(callSign => text.StartsWith($"{callSign},", StringComparison.OrdinalIgnoreCase)) is not { } callSign) { + // Sender must be a user + if (message.From is not { } from + || !HumanSender.TryCreate(from, out HumanSender? sender)) { aiCallCommand = null; return false; } - // Sender must be a user - if (message.From is not { - IsBot: false, - Id: long senderId, - FirstName: string senderFirstName, - LastName: var senderLastName - }) { + // Message must contain text or caption + if ((message.Text ?? message.Caption) is not { } text) { aiCallCommand = null; return false; } - string senderFullName = senderLastName is null - ? senderFirstName - : $"{senderFirstName} {senderLastName}"; + // Message must start with call sign + if (CALL_SIGNS.FirstOrDefault(callSign => text.StartsWith($"{callSign},", StringComparison.OrdinalIgnoreCase)) is not { } callSign) { + aiCallCommand = null; + return false; + } aiCallCommand = new( - messageId: message.MessageId, - chatId: message.Chat.Id, - chatType: message.Chat.Type, - chatTitle: message.Chat.Title, - senderId: senderId, - senderName: senderFullName, - commandPriority: commandPriority, + messageId: new(message.MessageId), + chat: chat, + sender: sender, text: text[(callSign.Length + 1)..].Trim(), imageFileId: message.Photo?.LastOrDefault()?.FileId ?? message.ReplyToMessage?.Sticker?.FileId, - replyToMessageId: message.ReplyToMessage?.MessageId, replyToMessage: message.ReplyToMessage is null ? null : NormalMessage.FromMessage(message.ReplyToMessage), diff --git a/BotNet.Commands/BotUpdate/Message/AIFollowUpMessage.cs b/BotNet.Commands/BotUpdate/Message/AIFollowUpMessage.cs index 6565b41..5f41566 100644 --- a/BotNet.Commands/BotUpdate/Message/AIFollowUpMessage.cs +++ b/BotNet.Commands/BotUpdate/Message/AIFollowUpMessage.cs @@ -1,46 +1,46 @@ using System.Diagnostics.CodeAnalysis; -using BotNet.Commands.CommandPrioritization; -using Telegram.Bot.Types.Enums; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.BotUpdate.Message { - public sealed record AIFollowUpMessage : MessageBase, ICommand { - public string CallSign { get; } + public sealed record AIFollowUpMessage : HumanMessageBase, ICommand { + public override AIResponseMessage ReplyToMessage => (AIResponseMessage)base.ReplyToMessage!; + public string CallSign => ReplyToMessage.CallSign; public AIFollowUpMessage( - int messageId, - long chatId, - ChatType chatType, - string? chatTitle, - long senderId, - string senderName, - CommandPriority commandPriority, + MessageId messageId, + ChatBase chat, + HumanSender sender, string text, string? imageFileId, - int? replyToMessageId, - MessageBase? replyToMessage, - string callSign + AIResponseMessage replyToMessage ) : base( messageId: messageId, - chatId: chatId, - chatType: chatType, - chatTitle: chatTitle, - senderId: senderId, - senderName: senderName, - commandPriority: commandPriority, + chat: chat, + sender: sender, text: text, imageFileId: imageFileId, - replyToMessageId: replyToMessageId, replyToMessage: replyToMessage - ) { - CallSign = callSign; - } + ) { } public static bool TryCreate( Telegram.Bot.Types.Message message, - CommandPriority commandPriority, IEnumerable thread, [NotNullWhen(true)] out AIFollowUpMessage? aiFollowUpMessage ) { + // Chat must be private or group + if (!ChatBase.TryCreate(message.Chat, out ChatBase? chat)) { + aiFollowUpMessage = null; + return false; + } + + // Sender must be a user + if (message.From is not { } from + || !HumanSender.TryCreate(from, out HumanSender? sender)) { + aiFollowUpMessage = null; + return false; + } + // Message must contain text or caption if ((message.Text ?? message.Caption) is not { } text) { aiFollowUpMessage = null; @@ -70,18 +70,12 @@ public static bool TryCreate( : $"{senderFirstName} {senderLastName}"; aiFollowUpMessage = new( - messageId: message.MessageId, - chatId: message.Chat.Id, - chatType: message.Chat.Type, - chatTitle: message.Chat.Title, - senderId: senderId, - senderName: senderFullName, - commandPriority: commandPriority, + messageId: new(message.MessageId), + chat: chat, + sender: sender, text: text, imageFileId: message.Photo?.FirstOrDefault()?.FileId, - replyToMessageId: message.ReplyToMessage?.MessageId, - replyToMessage: aiResponseMessage, - callSign: callSign + replyToMessage: aiResponseMessage ); return true; } diff --git a/BotNet.Commands/BotUpdate/Message/AIResponseMessage.cs b/BotNet.Commands/BotUpdate/Message/AIResponseMessage.cs index 6fdddc9..606a20f 100644 --- a/BotNet.Commands/BotUpdate/Message/AIResponseMessage.cs +++ b/BotNet.Commands/BotUpdate/Message/AIResponseMessage.cs @@ -1,33 +1,24 @@ -using BotNet.Commands.CommandPrioritization; -using Telegram.Bot.Types.Enums; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.BotUpdate.Message { public sealed record AIResponseMessage : MessageBase { public string CallSign { get; } private AIResponseMessage( - int messageId, - long chatId, - ChatType chatType, - string? chatTitle, - long senderId, - string senderName, + MessageId messageId, + ChatBase chat, + BotSender sender, string text, string? imageFileId, - int? replyToMessageId, - MessageBase? replyToMessage, + HumanMessageBase? replyToMessage, string callSign ) : base( messageId: messageId, - chatId: chatId, - chatType: chatType, - chatTitle: chatTitle, - senderId: senderId, - senderName: senderName, - commandPriority: CommandPriority.Void, + chat: chat, + sender: sender, text: text, imageFileId: imageFileId, - replyToMessageId: replyToMessageId, replyToMessage: replyToMessage ) { CallSign = callSign; @@ -35,20 +26,27 @@ string callSign public static AIResponseMessage FromMessage( Telegram.Bot.Types.Message message, - int replyToMessageId, + HumanMessageBase replyToMessage, string callSign ) { + // Chat must be private or group + if (!ChatBase.TryCreate(message.Chat, out ChatBase? chat)) { + throw new ArgumentException("Chat must be private or group.", nameof(message)); + } + + // Sender must be a bot + if (message.From is not { } from + || !BotSender.TryCreate(from, out BotSender? sender)) { + throw new ArgumentException("Sender must be bot.", nameof(message)); + } + return new( - messageId: message.MessageId, - chatId: message.Chat.Id, - chatType: message.Chat.Type, - chatTitle: message.Chat.Title, - senderId: message.From!.Id, - senderName: callSign, + messageId: new(message.MessageId), + chat: chat, + sender: sender, text: message.Text ?? message.Caption ?? "", imageFileId: message.Photo?.FirstOrDefault()?.FileId, - replyToMessageId: replyToMessageId, - replyToMessage: null, + replyToMessage: replyToMessage, callSign: callSign ); } diff --git a/BotNet.Commands/BotUpdate/Message/MessageBase.cs b/BotNet.Commands/BotUpdate/Message/MessageBase.cs index dd1deea..e071bd3 100644 --- a/BotNet.Commands/BotUpdate/Message/MessageBase.cs +++ b/BotNet.Commands/BotUpdate/Message/MessageBase.cs @@ -1,44 +1,69 @@ -using BotNet.Commands.CommandPrioritization; -using Telegram.Bot.Types.Enums; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.BotUpdate.Message { public abstract record MessageBase { - public int MessageId { get; private set; } - public long ChatId { get; private set; } - public ChatType ChatType { get; private set; } - public string? ChatTitle { get; private set; } - public long SenderId { get; private set; } - public string SenderName { get; private set; } - public CommandPriority CommandPriority { get; private set; } + public MessageId MessageId { get; private set; } + public ChatBase Chat { get; private set; } + public virtual SenderBase Sender { get; private set; } public string Text { get; private set; } public string? ImageFileId { get; private set; } - public int? ReplyToMessageId { get; private set; } - public MessageBase? ReplyToMessage { get; private set; } + public virtual MessageBase? ReplyToMessage { get; private set; } protected MessageBase( - int messageId, - long chatId, - ChatType chatType, - string? chatTitle, - long senderId, - string senderName, - CommandPriority commandPriority, + MessageId messageId, + ChatBase chat, + SenderBase sender, string text, string? imageFileId, - int? replyToMessageId, MessageBase? replyToMessage ) { MessageId = messageId; - ChatId = chatId; - ChatType = chatType; - ChatTitle = chatTitle; - SenderId = senderId; - SenderName = senderName; - CommandPriority = commandPriority; + Chat = chat; + Sender = sender; Text = text; ImageFileId = imageFileId; - ReplyToMessageId = replyToMessageId; ReplyToMessage = replyToMessage; } } + + public abstract record HumanMessageBase : MessageBase { + public override HumanSender Sender => (HumanSender)base.Sender; + + protected HumanMessageBase( + MessageId messageId, + ChatBase chat, + HumanSender sender, + string text, + string? imageFileId, + MessageBase? replyToMessage + ) : base( + messageId: messageId, + chat: chat, + sender: sender, + text: text, + imageFileId: imageFileId, + replyToMessage: replyToMessage + ) { } + } + + public abstract record BotMessageBase : MessageBase { + public override BotSender Sender => (BotSender)base.Sender; + + protected BotMessageBase( + MessageId messageId, + ChatBase chat, + BotSender sender, + string text, + string? imageFileId, + MessageBase? replyToMessage + ) : base( + messageId: messageId, + chat: chat, + sender: sender, + text: text, + imageFileId: imageFileId, + replyToMessage: replyToMessage + ) { } + } } diff --git a/BotNet.Commands/BotUpdate/Message/MessageId.cs b/BotNet.Commands/BotUpdate/Message/MessageId.cs new file mode 100644 index 0000000..a5e43fe --- /dev/null +++ b/BotNet.Commands/BotUpdate/Message/MessageId.cs @@ -0,0 +1,6 @@ +namespace BotNet.Commands.BotUpdate.Message { + public readonly record struct MessageId(int Value) { + public static implicit operator int(MessageId messageId) => messageId.Value; + public static implicit operator MessageId(Telegram.Bot.Types.MessageId messageId) => new(messageId.Id); + } +} diff --git a/BotNet.Commands/BotUpdate/Message/NormalMessage.cs b/BotNet.Commands/BotUpdate/Message/NormalMessage.cs index 26769bf..49beaf2 100644 --- a/BotNet.Commands/BotUpdate/Message/NormalMessage.cs +++ b/BotNet.Commands/BotUpdate/Message/NormalMessage.cs @@ -1,57 +1,45 @@ -using BotNet.Commands.CommandPrioritization; -using Telegram.Bot.Types.Enums; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.BotUpdate.Message { public sealed record NormalMessage : MessageBase { private NormalMessage( - int messageId, - long chatId, - ChatType chatType, - string? chatTitle, - long senderId, - string senderName, + MessageId messageId, + ChatBase chat, + SenderBase sender, string text, string? imageFileId, - int? replyToMessageId, MessageBase? replyToMessage ) : base( messageId: messageId, - chatId: chatId, - chatType: chatType, - chatTitle: chatTitle, - senderId: senderId, - senderName: senderName, - commandPriority: CommandPriority.Void, + chat: chat, + sender: sender, text: text, imageFileId: imageFileId, - replyToMessageId: replyToMessageId, replyToMessage: replyToMessage ) { } public static NormalMessage FromMessage(Telegram.Bot.Types.Message message) { + // Chat must be private or group + if (!ChatBase.TryCreate(message.Chat, out ChatBase? chat)) { + throw new ArgumentException("Chat must be private or group.", nameof(message)); + } + // Sender must not be null - if (message.From is not { - Id: long senderId, - FirstName: string senderFirstName, - LastName: var senderLastName - }) { + if (message.From is not { } from) { throw new ArgumentException("Message must have a sender.", nameof(message)); } - string senderFullName = message.From is null - ? senderFirstName - : $"{senderFirstName} {senderLastName}"; - return new( - messageId: message.MessageId, - chatId: message.Chat.Id, - chatType: message.Chat.Type, - chatTitle: message.Chat.Title, - senderId: senderId, - senderName: senderFullName, + messageId: new(message.MessageId), + chat: chat, + sender: HumanSender.TryCreate(from, out HumanSender? humanSender) + ? humanSender + : BotSender.TryCreate(from, out BotSender? botSender) + ? botSender + : throw new ArgumentException("Unknown sender type.", nameof(message)), text: message.Text ?? "", imageFileId: message.Photo?.LastOrDefault()?.FileId ?? message.Sticker?.FileId, - replyToMessageId: message.ReplyToMessage?.MessageId, replyToMessage: message.ReplyToMessage is null ? null : NormalMessage.FromMessage(message.ReplyToMessage) diff --git a/BotNet.Commands/BotUpdate/Message/SlashCommand.cs b/BotNet.Commands/BotUpdate/Message/SlashCommand.cs index b3b8b4a..d43be36 100644 --- a/BotNet.Commands/BotUpdate/Message/SlashCommand.cs +++ b/BotNet.Commands/BotUpdate/Message/SlashCommand.cs @@ -1,35 +1,26 @@ using System.Diagnostics.CodeAnalysis; -using BotNet.Commands.CommandPrioritization; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; using Telegram.Bot.Types.Enums; namespace BotNet.Commands.BotUpdate.Message { - public sealed record SlashCommand : MessageBase, ICommand { + public sealed record SlashCommand : HumanMessageBase, ICommand { public string Command { get; } private SlashCommand( - int messageId, - long chatId, - ChatType chatType, - string? chatTitle, - long senderId, - string senderName, - CommandPriority commandPriority, + MessageId messageId, + ChatBase chat, + HumanSender sender, string text, string? imageFileId, - int? replyToMessageId, MessageBase? replyToMessage, string command ) : base( messageId: messageId, - chatId: chatId, - chatType: chatType, - chatTitle: chatTitle, - senderId: senderId, - senderName: senderName, - commandPriority: commandPriority, + chat: chat, + sender: sender, text: text, imageFileId: imageFileId, - replyToMessageId: replyToMessageId, replyToMessage: replyToMessage ) { ArgumentNullException.ThrowIfNull(command); @@ -42,7 +33,6 @@ string command public static bool TryCreate( Telegram.Bot.Types.Message message, string botUsername, - CommandPriority commandPriority, [NotNullWhen(true)] out SlashCommand? slashCommand ) { // Message must start with a slash command @@ -55,6 +45,19 @@ public static bool TryCreate( return false; } + // Chat must be private or group + if (!ChatBase.TryCreate(message.Chat, out ChatBase? chat)) { + slashCommand = null; + return false; + } + + // Sender must be a user + if (message.From is not { } from + || !HumanSender.TryCreate(from, out HumanSender? sender)) { + slashCommand = null; + return false; + } + // Message must have text or a caption if ((message.Text ?? message.Caption) is not { } text || text.Length < commandLength) { @@ -77,32 +80,12 @@ public static bool TryCreate( commandText = commandText[..ampersandPos]; } - // Sender must be a user - if (message.From is not { - IsBot: false, - Id: long senderId, - FirstName: string senderFirstName, - LastName: var senderLastName - }) { - slashCommand = null; - return false; - } - - string senderFullName = senderLastName is null - ? senderFirstName - : $"{senderFirstName} {senderLastName}"; - slashCommand = new( - messageId: message.MessageId, - chatId: message.Chat.Id, - chatType: message.Chat.Type, - chatTitle: message.Chat.Title, - senderId: senderId, - senderName: senderFullName, - commandPriority: commandPriority, + messageId: new(message.MessageId), + chat: chat, + sender: sender, text: arg, imageFileId: message.Photo?.LastOrDefault()?.FileId, - replyToMessageId: message.ReplyToMessage?.MessageId, replyToMessage: message.ReplyToMessage is null ? null : NormalMessage.FromMessage(message.ReplyToMessage), diff --git a/BotNet.Commands/ChatAggregate/Chat.cs b/BotNet.Commands/ChatAggregate/Chat.cs new file mode 100644 index 0000000..0f2bfe6 --- /dev/null +++ b/BotNet.Commands/ChatAggregate/Chat.cs @@ -0,0 +1,79 @@ +using System.Diagnostics.CodeAnalysis; +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 static bool TryCreate( + Telegram.Bot.Types.Chat telegramChat, + [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 } => GroupChat.FromTelegramChat(telegramChat), + _ => null + }; + return chat is not null; + } + } + + public sealed record PrivateChat : ChatBase { + private PrivateChat( + ChatId id + ) : base(id, null) { } + + public static PrivateChat FromTelegramChat( + Telegram.Bot.Types.Chat telegramChat + ) { + if (telegramChat is not { + Id: long chatId, + Type: ChatType.Private + }) { + throw new ArgumentException("Telegram chat must be private."); + } + + return new( + id: new ChatId(chatId) + ); + } + } + + public record GroupChat : ChatBase { + protected GroupChat( + ChatId id, + string title + ) : base(id, title) { } + + public static GroupChat FromTelegramChat( + Telegram.Bot.Types.Chat telegramChat + ) { + if (telegramChat is not { + Id: long chatId, + Title: string chatTitle, + Type: ChatType.Group or ChatType.Supergroup + }) { + throw new ArgumentException("Telegram chat must be either a group or a supergroup."); + } + + return new( + id: new ChatId(chatId), + title: chatTitle + ); + } + } + + public sealed record HomeGroupChat( + ChatId Id, + string Title + ) : GroupChat(Id, Title); +} diff --git a/BotNet.Commands/ChatAggregate/ChatId.cs b/BotNet.Commands/ChatAggregate/ChatId.cs new file mode 100644 index 0000000..39e3e32 --- /dev/null +++ b/BotNet.Commands/ChatAggregate/ChatId.cs @@ -0,0 +1,14 @@ +namespace BotNet.Commands.ChatAggregate { + public readonly record struct ChatId(long Value) { + public static implicit operator long(ChatId chatId) => chatId.Value; + + public static implicit operator ChatId(Telegram.Bot.Types.ChatId chatId) { + if (chatId.Identifier is null) { + throw new ArgumentException("ChatId.Identifier is null"); + } + return new(chatId.Identifier.Value); + } + + public static implicit operator Telegram.Bot.Types.ChatId(ChatId chatId) => new(chatId.Value); + } +} diff --git a/BotNet.Commands/Eval/EvalCommand.cs b/BotNet.Commands/Eval/EvalCommand.cs index a28417e..c1986e2 100644 --- a/BotNet.Commands/Eval/EvalCommand.cs +++ b/BotNet.Commands/Eval/EvalCommand.cs @@ -1,24 +1,25 @@ using BotNet.Commands.Common; using Telegram.Bot.Types.Enums; using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; namespace BotNet.Commands.Eval { public sealed record EvalCommand : ICommand { public string Command { get; } public string Code { get; } - public long ChatId { get; } - public int CodeMessageId { get; } + public MessageId CodeMessageId { get; } + public ChatBase Chat { get; } private EvalCommand( string command, string code, - long chatId, - int codeMessageId + MessageId codeMessageId, + ChatBase chat ) { Command = command; Code = code; - ChatId = chatId; CodeMessageId = codeMessageId; + Chat = chat; } public static EvalCommand FromSlashCommand(SlashCommand slashCommand) { @@ -52,8 +53,8 @@ public static EvalCommand FromSlashCommand(SlashCommand slashCommand) { return new( command: slashCommand.Command, code: code, - chatId: slashCommand.ChatId, - codeMessageId: codeMessageId + codeMessageId: new(codeMessageId), + chat: slashCommand.Chat ); } } diff --git a/BotNet.Commands/Exec/ExecCommand.cs b/BotNet.Commands/Exec/ExecCommand.cs index 2241e4c..23261f6 100644 --- a/BotNet.Commands/Exec/ExecCommand.cs +++ b/BotNet.Commands/Exec/ExecCommand.cs @@ -1,27 +1,28 @@ using BotNet.Commands.Common; using Telegram.Bot.Types.Enums; using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; namespace BotNet.Commands.Exec { public sealed record ExecCommand : ICommand { public string PistonLanguageIdentifier { get; } public string HighlightLanguageIdentifier { get; } public string Code { get; } - public long ChatId { get; } - public int CodeMessageId { get; } + public MessageId CodeMessageId { get; } + public ChatBase Chat { get; } private ExecCommand( string pistonLanguageIdentifier, string highlightLanguageIdentifier, string code, - long chatId, - int codeMessageId + MessageId codeMessageId, + ChatBase chat ) { PistonLanguageIdentifier = pistonLanguageIdentifier; HighlightLanguageIdentifier = highlightLanguageIdentifier; Code = code; - ChatId = chatId; CodeMessageId = codeMessageId; + Chat = chat; } public static ExecCommand FromSlashCommand(SlashCommand slashCommand) { @@ -107,8 +108,8 @@ public static ExecCommand FromSlashCommand(SlashCommand slashCommand) { pistonLanguageIdentifier: pistonLanguageIdentifier, highlightLanguageIdentifier: highlightLanguageIdentifier, code: code, - chatId: slashCommand.ChatId, - codeMessageId: codeMessageId + codeMessageId: new(codeMessageId), + chat: slashCommand.Chat ); } } diff --git a/BotNet.Commands/FlipFlop/FlipFlopCommand.cs b/BotNet.Commands/FlipFlop/FlipFlopCommand.cs index 3bd0470..6b20dca 100644 --- a/BotNet.Commands/FlipFlop/FlipFlopCommand.cs +++ b/BotNet.Commands/FlipFlop/FlipFlopCommand.cs @@ -1,24 +1,25 @@ using BotNet.Commands.Common; using Telegram.Bot.Types.Enums; using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; namespace BotNet.Commands.FlipFlop { public sealed record FlipFlopCommand : ICommand { public string Command { get; } - public long ChatId { get; } - public int ImageMessageId { get; } public string ImageFileId { get; } + public MessageId ImageMessageId { get; } + public ChatBase Chat { get; } private FlipFlopCommand( string command, - long chatId, - int imageMessageId, - string imageFileId + string imageFileId, + MessageId imageMessageId, + ChatBase chat ) { Command = command; - ChatId = chatId; - ImageMessageId = imageMessageId; ImageFileId = imageFileId; + ImageMessageId = imageMessageId; + Chat = chat; } public static FlipFlopCommand FromSlashCommand(SlashCommand slashCommand) { @@ -50,9 +51,9 @@ public static FlipFlopCommand FromSlashCommand(SlashCommand slashCommand) { return new( command: slashCommand.Command, - chatId: slashCommand.ChatId, + imageFileId: slashCommand.ReplyToMessage.ImageFileId, imageMessageId: slashCommand.ReplyToMessage.MessageId, - imageFileId: slashCommand.ReplyToMessage.ImageFileId + chat: slashCommand.Chat ); } } diff --git a/BotNet.Commands/Fuck/FuckCommand.cs b/BotNet.Commands/Fuck/FuckCommand.cs index 453daf7..53696c4 100644 --- a/BotNet.Commands/Fuck/FuckCommand.cs +++ b/BotNet.Commands/Fuck/FuckCommand.cs @@ -1,21 +1,22 @@ using BotNet.Commands.Common; using Telegram.Bot.Types.Enums; using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; namespace BotNet.Commands.Fuck { public sealed record FuckCommand : ICommand { public string Code { get; } - public long ChatId { get; } - public int CodeMessageId { get; } + public MessageId CodeMessageId { get; } + public ChatBase Chat { get; } private FuckCommand( string code, - long chatId, - int codeMessageId + MessageId codeMessageId, + ChatBase chat ) { Code = code; - ChatId = chatId; CodeMessageId = codeMessageId; + Chat = chat; } public static FuckCommand FromSlashCommand(SlashCommand slashCommand) { @@ -46,8 +47,8 @@ public static FuckCommand FromSlashCommand(SlashCommand slashCommand) { return new( code: code, - chatId: slashCommand.ChatId, - codeMessageId: codeMessageId + codeMessageId: new(codeMessageId), + chat: slashCommand.Chat ); } } diff --git a/BotNet.Commands/GoogleMaps/MapCommand.cs b/BotNet.Commands/GoogleMaps/MapCommand.cs index 3fe969c..9861d51 100644 --- a/BotNet.Commands/GoogleMaps/MapCommand.cs +++ b/BotNet.Commands/GoogleMaps/MapCommand.cs @@ -1,24 +1,26 @@ using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; using BotNet.Commands.Common; +using BotNet.Commands.SenderAggregate; using Telegram.Bot.Types.Enums; namespace BotNet.Commands.GoogleMaps { public sealed record MapCommand : ICommand { public string PlaceName { get; } - public int CommandMessageId { get; } - public long ChatId { get; } - public long SenderId { get; } + public MessageId CommandMessageId { get; } + public ChatBase Chat { get; } + public HumanSender Sender { get; } private MapCommand( string placeName, - int commandMessageId, - long chatId, - long senderId + MessageId commandMessageId, + ChatBase chat, + HumanSender sender ) { PlaceName = placeName; CommandMessageId = commandMessageId; - ChatId = chatId; - SenderId = senderId; + Chat = chat; + Sender = sender; } public static MapCommand FromSlashCommand(SlashCommand slashCommand) { @@ -39,8 +41,8 @@ public static MapCommand FromSlashCommand(SlashCommand slashCommand) { return new( placeName: slashCommand.Text, commandMessageId: slashCommand.MessageId, - chatId: slashCommand.ChatId, - senderId: slashCommand.SenderId + chat: slashCommand.Chat, + sender: slashCommand.Sender ); } } diff --git a/BotNet.Commands/Humor/HumorCommand.cs b/BotNet.Commands/Humor/HumorCommand.cs index 4eea895..723ab6f 100644 --- a/BotNet.Commands/Humor/HumorCommand.cs +++ b/BotNet.Commands/Humor/HumorCommand.cs @@ -1,19 +1,21 @@ using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.Humor { public sealed record HumorCommand : ICommand { - public long ChatId { get; } - public int CommandMessageId { get; } - public long SenderId { get; } + public MessageId CommandMessageId { get; } + public ChatBase Chat { get; } + public HumanSender Sender { get; } private HumorCommand( - long chatId, - int commandMessageId, - long senderId + MessageId commandMessageId, + ChatBase chat, + HumanSender sender ) { - ChatId = chatId; CommandMessageId = commandMessageId; - SenderId = senderId; + Chat = chat; + Sender = sender; } public static HumorCommand FromSlashCommand(SlashCommand slashCommand) { @@ -22,9 +24,9 @@ public static HumorCommand FromSlashCommand(SlashCommand slashCommand) { } return new( - chatId: slashCommand.ChatId, commandMessageId: slashCommand.MessageId, - senderId: slashCommand.SenderId + chat: slashCommand.Chat, + sender: slashCommand.Sender ); } } diff --git a/BotNet.Commands/ITelegramMessageCache.cs b/BotNet.Commands/ITelegramMessageCache.cs index c559253..d3ddded 100644 --- a/BotNet.Commands/ITelegramMessageCache.cs +++ b/BotNet.Commands/ITelegramMessageCache.cs @@ -1,10 +1,11 @@ using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; namespace BotNet.Commands { public interface ITelegramMessageCache { void Add(MessageBase message); - MessageBase? GetOrDefault(int messageId, long chatId); - IEnumerable GetThread(int messageId, long chatId); + MessageBase? GetOrDefault(MessageId messageId, ChatId chatId); + IEnumerable GetThread(MessageId messageId, ChatId chatId); IEnumerable GetThread(MessageBase firstMessage); } } diff --git a/BotNet.Commands/Pop/PopCommand.cs b/BotNet.Commands/Pop/PopCommand.cs index 7f49190..41e3f4a 100644 --- a/BotNet.Commands/Pop/PopCommand.cs +++ b/BotNet.Commands/Pop/PopCommand.cs @@ -1,11 +1,12 @@ using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; namespace BotNet.Commands.Pop { public sealed record PopCommand : ICommand { - public long ChatId { get; } + public ChatBase Chat { get; } - private PopCommand(long chatId) { - ChatId = chatId; + private PopCommand(ChatBase chat) { + Chat = chat; } public static PopCommand FromSlashCommand(SlashCommand slashCommand) { @@ -14,7 +15,7 @@ public static PopCommand FromSlashCommand(SlashCommand slashCommand) { } return new( - chatId: slashCommand.ChatId + chat: slashCommand.Chat ); } } diff --git a/BotNet.Commands/Primbon/PrimbonCommand.cs b/BotNet.Commands/Primbon/PrimbonCommand.cs index b36612b..5cc46c4 100644 --- a/BotNet.Commands/Primbon/PrimbonCommand.cs +++ b/BotNet.Commands/Primbon/PrimbonCommand.cs @@ -1,24 +1,26 @@ using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; using BotNet.Commands.Common; +using BotNet.Commands.SenderAggregate; using Telegram.Bot.Types.Enums; namespace BotNet.Commands.Primbon { public sealed record PrimbonCommand : ICommand { - public long ChatId { get; } - public int CommandMessageId { get; } - public long SenderId { get; } public DateOnly Date { get; } + public MessageId CommandMessageId { get; } + public ChatBase Chat { get; } + public HumanSender Sender { get; } private PrimbonCommand( - long chatId, - int commandMessageId, - long senderId, - DateOnly date + DateOnly date, + MessageId commandMessageId, + ChatBase chat, + HumanSender sender ) { - ChatId = chatId; - CommandMessageId = commandMessageId; - SenderId = senderId; Date = date; + CommandMessageId = commandMessageId; + Chat = chat; + Sender = sender; } public static PrimbonCommand FromSlashCommand(SlashCommand slashCommand) { @@ -59,10 +61,10 @@ public static PrimbonCommand FromSlashCommand(SlashCommand slashCommand) { } return new( - chatId: slashCommand.ChatId, + date: date, commandMessageId: slashCommand.MessageId, - senderId: slashCommand.SenderId, - date: date + chat: slashCommand.Chat, + sender: slashCommand.Sender ); } } diff --git a/BotNet.Commands/Privilege/PrivilegeCommand.cs b/BotNet.Commands/Privilege/PrivilegeCommand.cs index 72014e9..7ca3e6e 100644 --- a/BotNet.Commands/Privilege/PrivilegeCommand.cs +++ b/BotNet.Commands/Privilege/PrivilegeCommand.cs @@ -1,44 +1,28 @@ using BotNet.Commands.BotUpdate.Message; -using BotNet.Commands.CommandPrioritization; -using Telegram.Bot.Types.Enums; +using BotNet.Commands.ChatAggregate; +using BotNet.Commands.SenderAggregate; namespace BotNet.Commands.Privilege { public sealed record PrivilegeCommand : ICommand { - public int CommandMessageId { get; } - public long ChatId { get; } - public ChatType ChatType { get; } - public string? ChatTitle { get; } - public long SenderId { get; } - public string SenderName { get; } - public CommandPriority CommandPriority { get; } + public MessageId CommandMessageId { get; } + public ChatBase Chat { get; } + public HumanSender Sender { get; } private PrivilegeCommand( - int commandMessageId, - long chatId, - ChatType chatType, - string? chatTitle, - long senderId, - string senderName, - CommandPriority commandPriority + MessageId commandMessageId, + ChatBase chat, + HumanSender sender ) { CommandMessageId = commandMessageId; - ChatId = chatId; - ChatType = chatType; - ChatTitle = chatTitle; - SenderId = senderId; - SenderName = senderName; - CommandPriority = commandPriority; + Chat = chat; + Sender = sender; } public static PrivilegeCommand FromSlashCommand(SlashCommand command) { return new( commandMessageId: command.MessageId, - chatId: command.ChatId, - chatType: command.ChatType, - chatTitle: command.ChatTitle, - senderId: command.SenderId, - senderName: command.SenderName, - commandPriority: command.CommandPriority + chat: command.Chat, + sender: command.Sender ); } } diff --git a/BotNet.Commands/SenderAggregate/Sender.cs b/BotNet.Commands/SenderAggregate/Sender.cs new file mode 100644 index 0000000..5495f7e --- /dev/null +++ b/BotNet.Commands/SenderAggregate/Sender.cs @@ -0,0 +1,71 @@ +using System.Diagnostics.CodeAnalysis; + +namespace BotNet.Commands.SenderAggregate { + public abstract record SenderBase( + SenderId Id, + string Name + ) { + public abstract string ChatGPTRole { get; } + } + + public record HumanSender( + SenderId Id, + string Name + ) : SenderBase(Id, Name) { + public override string ChatGPTRole => "user"; + + public static bool TryCreate( + Telegram.Bot.Types.User user, + [NotNullWhen(true)] out HumanSender? humanSender + ) { + if (user is { + IsBot: false, + Id: long senderId, + FirstName: string senderFirstName, + LastName: var senderLastName + }) { + humanSender = new HumanSender( + Id: senderId, + Name: senderLastName is { } ? $"{senderFirstName} {senderLastName}" : senderFirstName + ); + return true; + } + + humanSender = null; + return false; + } + } + + public sealed record BotSender( + SenderId Id, + string Name + ) : SenderBase(Id, Name) { + public override string ChatGPTRole => "assistant"; + + public static bool TryCreate( + Telegram.Bot.Types.User user, + [NotNullWhen(true)] out BotSender? botSender + ) { + if (user is { + IsBot: true, + Id: long senderId, + FirstName: string senderFirstName, + LastName: var senderLastName + }) { + botSender = new BotSender( + Id: senderId, + Name: senderLastName is { } ? $"{senderFirstName} {senderLastName}" : senderFirstName + ); + return true; + } + + botSender = null; + return false; + } + } + + public sealed record VIPSender( + SenderId Id, + string Name + ) : HumanSender(Id, Name); +} diff --git a/BotNet.Commands/SenderAggregate/SenderId.cs b/BotNet.Commands/SenderAggregate/SenderId.cs new file mode 100644 index 0000000..fc9a35a --- /dev/null +++ b/BotNet.Commands/SenderAggregate/SenderId.cs @@ -0,0 +1,6 @@ +namespace BotNet.Commands.SenderAggregate { + public readonly record struct SenderId(long Value) { + public static implicit operator long(SenderId senderId) => senderId.Value; + public static implicit operator SenderId(long senderId) => new(senderId); + } +} diff --git a/BotNet.Commands/Weather/WeatherCommand.cs b/BotNet.Commands/Weather/WeatherCommand.cs index 11f3462..4b33e20 100644 --- a/BotNet.Commands/Weather/WeatherCommand.cs +++ b/BotNet.Commands/Weather/WeatherCommand.cs @@ -1,24 +1,26 @@ using BotNet.Commands.BotUpdate.Message; +using BotNet.Commands.ChatAggregate; using BotNet.Commands.Common; +using BotNet.Commands.SenderAggregate; using Telegram.Bot.Types.Enums; namespace BotNet.Commands.Weather { public sealed record WeatherCommand : ICommand { public string CityName { get; } - public int CommandMessageId { get; } - public long ChatId { get; } - public long SenderId { get; } + public MessageId CommandMessageId { get; } + public ChatBase Chat { get; } + public HumanSender Sender { get; } public WeatherCommand( string cityName, - int commandMessageId, - long chatId, - long senderId + MessageId commandMessageId, + ChatBase chat, + HumanSender sender ) { CityName = cityName; CommandMessageId = commandMessageId; - ChatId = chatId; - SenderId = senderId; + Chat = chat; + Sender = sender; } public static WeatherCommand FromSlashCommand(SlashCommand slashCommand) { @@ -39,8 +41,8 @@ public static WeatherCommand FromSlashCommand(SlashCommand slashCommand) { return new( cityName: slashCommand.Text, commandMessageId: slashCommand.MessageId, - chatId: slashCommand.ChatId, - senderId: slashCommand.SenderId + chat: slashCommand.Chat, + sender: slashCommand.Sender ); } }