From a72f0ee43d3d9110c2c47d2fd47cd147c4f1aa39 Mon Sep 17 00:00:00 2001 From: Lorenzo Rossi Date: Sun, 25 Aug 2024 12:49:29 +0200 Subject: [PATCH] Differentiate between unknown command and parse error (#14) Co-authored-by: Stefano Pigozzi --- src/services/telegram/commands/mod.rs | 70 +++++++++++++++++--- src/services/telegram/mod.rs | 95 +++++++++++++++++++-------- src/utils/italian.rs | 12 ++++ src/utils/mod.rs | 1 + 4 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 src/utils/italian.rs diff --git a/src/services/telegram/commands/mod.rs b/src/services/telegram/commands/mod.rs index a0aa1970..36f8dcdb 100644 --- a/src/services/telegram/commands/mod.rs +++ b/src/services/telegram/commands/mod.rs @@ -4,14 +4,15 @@ use std::sync::Arc; use anyhow::{Context, Error}; -use teloxide::Bot; use teloxide::payloads::SendMessageSetters; use teloxide::requests::Requester; use teloxide::types::{Message, ReplyParameters}; use teloxide::utils::command::BotCommands; +use teloxide::Bot; use crate::services::telegram::dependencies::interface_database::DatabaseInterface; use crate::utils::anyhow_result::AnyResult; +use crate::utils::italian::countable_noun_suffix; pub mod start; pub mod fortune; @@ -111,7 +112,7 @@ impl Command { log::trace!("Delegating error handling to error handler..."); let result2 = match result1.as_ref() { Ok(_) => return, - Err(e1) => self.handle_error(&bot, &message, e1).await + Err(e1) => self.handle_error_command(&bot, &message, e1).await }; let e1 = result1.unwrap_err(); @@ -129,12 +130,49 @@ impl Command { Ok(()) } - pub async fn handle_unknown(bot: Bot, message: Message) -> CommandResult { - log::debug!("Received an unknown command or an invalid syntax: {:?}", message.text()); + pub async fn handle_malformed_simple(bot: Bot, message: Message, expected: usize, found: usize) -> CommandResult { + log::debug!("Received a malformed command: {:?}", message.text()); log::trace!("Sending error message..."); + let text = format!( + "⚠️ Il comando si aspetta {} argoment{}, ma ne ha ricevut{} solo {}.", + expected, + countable_noun_suffix(expected, "o", "i"), + countable_noun_suffix(found, "o", "i"), + found, + ); let _reply = bot - .send_message(message.chat.id, "⚠️ Comando sconosciuto o sintassi non valida.") + .send_message(message.chat.id, text) + .reply_parameters(ReplyParameters::new(message.id)) + .await + .context("Non è stato possibile inviare il messaggio di errore.")?; + + log::trace!("Successfully handled malformed command!"); + Ok(()) + } + + pub async fn handle_malformed_complex(bot: Bot, message: Message) -> CommandResult { + log::debug!("Received a malformed command: {:?}", message.text()); + + log::trace!("Sending error message..."); + let text = "⚠️ Il comando si aspetta una sintassi diversa da quella che ha ricevuto."; + let _reply = bot + .send_message(message.chat.id, text) + .reply_parameters(ReplyParameters::new(message.id)) + .await + .context("Non è stato possibile inviare il messaggio di errore.")?; + + log::trace!("Successfully handled malformed command!"); + Ok(()) + } + + pub async fn handle_unknown(bot: Bot, message: Message) -> CommandResult { + log::debug!("Received an unknown command: {:?}", message.text()); + + log::trace!("Sending error message..."); + let text = "⚠️ Il comando specificato non esiste."; + let _reply = bot + .send_message(message.chat.id, text) .reply_parameters(ReplyParameters::new(message.id)) .await .context("Non è stato possibile inviare il messaggio di errore.")?; @@ -143,7 +181,22 @@ impl Command { Ok(()) } - async fn handle_error(&self, bot: &Bot, message: &Message, error: &Error) -> CommandResult { + pub async fn handle_error_parse(bot: &Bot, message: &Message, error: &Error) -> CommandResult { + log::debug!("Encountered a parsing error while parsing: {:?}", message.text()); + + log::trace!("Sending error message..."); + let text = format!("⚠️ {error}"); + let _reply = bot + .send_message(message.chat.id, text) + .reply_parameters(ReplyParameters::new(message.id)) + .await + .context("Non è stato possibile inviare il messaggio di errore.")?; + + log::trace!("Successfully handled malparsed command!"); + Ok(()) + } + + pub async fn handle_error_command(&self, bot: &Bot, message: &Message, error: &Error) -> CommandResult { log::debug!( "Command message in {:?} with id {:?} and contents {:?} errored out with `{:?}`", &message.chat.id, @@ -153,8 +206,9 @@ impl Command { ); log::trace!("Sending error message..."); + let text = format!("⚠️ {error}"); let _reply = bot - .send_message(message.chat.id, format!("⚠️ {error}")) + .send_message(message.chat.id, text) .reply_parameters(ReplyParameters::new(message.id)) .await .context("Non è stato possibile inviare il messaggio di errore.")?; @@ -163,7 +217,7 @@ impl Command { Ok(()) } - async fn handle_fatal(&self, _bot: &Bot, message: &Message, error1: &Error, error2: &Error) -> CommandResult { + pub async fn handle_fatal(&self, _bot: &Bot, message: &Message, error1: &Error, error2: &Error) -> CommandResult { log::error!( "Command message in {:?} with id {:?} and contents {:?} errored out with `{:?}`, and it was impossible to handle the error because of `{:?}`", &message.chat.id, diff --git a/src/services/telegram/mod.rs b/src/services/telegram/mod.rs index 085360bc..a5b57928 100644 --- a/src/services/telegram/mod.rs +++ b/src/services/telegram/mod.rs @@ -1,15 +1,14 @@ use std::sync::Arc; use anyhow::Context; -use regex::Regex; +use commands::Command; +use dependencies::interface_database::DatabaseInterface; +use keyboard_callbacks::KeyboardCallback; use teloxide::dispatching::DefaultKey; use teloxide::dptree::entry; use teloxide::prelude::*; use teloxide::types::{Me, ParseMode}; - -use commands::Command; -use dependencies::interface_database::DatabaseInterface; -use keyboard_callbacks::KeyboardCallback; +use teloxide::utils::command::{BotCommands, ParseError}; use crate::utils::anyhow_result::AnyResult; use crate::utils::telegram_string::TelegramEscape; @@ -102,16 +101,74 @@ impl TelegramService { .context("Aggiornamento dei comandi del bot non riuscito.") } + async fn handle_message(bot: Bot, me: Me, message: Message, database: Arc) -> AnyResult<()> { + log::debug!("Handling message: {message:#?}"); + + log::trace!("Accessing message text..."); + let text = match message.text() { + None => { + log::trace!("Message has no text; skipping it."); + return Ok(()) + } + Some(text) => { + log::trace!("Message has text: {text:?}"); + text + } + }; + + log::trace!("Retrieving bot's username..."); + let username = me.username(); + + log::trace!("Parsing message text {text:?} as {username:?}..."); + let command = match Command::parse(text, username) { + Ok(command) => { + log::trace!("Message text parsed successfully as: {command:?}"); + command + } + Err(ParseError::WrongBotName(receiver)) => { + log::debug!("Message is meant to be sent to {receiver:?}, while I'm running as {username:?}; skipping it."); + return Ok(()); + } + Err(ParseError::TooFewArguments { expected, found, .. }) | + Err(ParseError::TooManyArguments { expected, found, .. }) => { + log::debug!("Message text is a command with {found} arguments, but the command expected {expected}; handling as a malformed command."); + Command::handle_malformed_simple(bot, message, expected, found).await + .context("Impossibile gestire comando malformato semplice.")?; + return Ok(()); + } + Err(ParseError::IncorrectFormat(e)) => { + log::debug!("Message text is a command with a custom format, but the parser returned the error {e:?}; handling as a malformed command."); + Command::handle_malformed_complex(bot, message).await + .context("Impossibile gestire comando malformato complesso.")?; + return Ok(()); + } + Err(ParseError::UnknownCommand(command)) => { + log::debug!("Message text is command not present in the commands list {command:?}; handling it as an unknown command."); + Command::handle_unknown(bot, message).await + .context("Impossibile gestire comando sconosciuto.")?; + return Ok(()); + } + Err(ParseError::Custom(e)) => { + log::debug!("Message text is a command, but the parser raised custom error {e:?}; handling it as a custom error."); + let error = anyhow::format_err!(e); + Command::handle_error_parse(&bot, &message, &error).await + .context("Impossibile gestire comando con errore di parsing.")?; + return Ok(()); + } + }; + + command.handle_self(bot, message, database).await + .context("Impossibile gestire errore restituito dal comando.")?; + + Ok(()) + } + fn dispatcher(&mut self) -> Dispatcher { log::debug!("Building dispatcher..."); let bot_name = self.me.user.username.as_ref().unwrap(); log::trace!("Bot username is: @{bot_name:?}"); - log::trace!("Determining pseudo-command regex..."); - let regex = Regex::new(&format!(r"^/[a-z0-9_]+(?:@{bot_name})?(?:\s+.*)?$")).unwrap(); - log::trace!("Pseudo-command regex is: {regex:?}"); - let database = Arc::new(DatabaseInterface::new(self.database_url.clone())); log::trace!("Building dispatcher..."); @@ -121,24 +178,8 @@ impl TelegramService { entry() // Messages .branch(Update::filter_message() - // Pseudo-commands - .branch(entry() - // Only process commands matching the pseudo-command regex - .filter(move |message: Message| -> bool { - message - .text() - .is_some_and(|text| regex.is_match(text)) - }) - // Commands - .branch(entry() - // Only process commands matching a valid command, and parse their arguments - .filter_command::() - // Delegate handling - .endpoint(Command::handle_self) - ) - // No valid command was found - .endpoint(Command::handle_unknown) - ) + // Handle incoming messages + .endpoint(Self::handle_message) ) // Inline keyboard .branch(Update::filter_callback_query() diff --git a/src/utils/italian.rs b/src/utils/italian.rs new file mode 100644 index 00000000..61018d3a --- /dev/null +++ b/src/utils/italian.rs @@ -0,0 +1,12 @@ +use std::ops::Add; + +pub fn countable_noun_suffix(count: T, singular: &'static str, plural: &'static str) + -> &'static str +where + T: Default + Add + PartialEq, +{ + match count == (T::default() + 1) { + true => singular, + false => plural, + } +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 6d912bd6..dca40308 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,3 +4,4 @@ pub mod time; pub mod version; pub mod anyhow_result; pub mod telegram_string; +pub mod italian;