Skip to content

Differentiate between unknown command and parse error #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 61 additions & 7 deletions src/services/telegram/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -129,12 +130,49 @@ impl Command {
Ok(())
}

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, 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 or an invalid syntax: {:?}", message.text());
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, "⚠️ 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.")?;
Expand All @@ -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,
Expand All @@ -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.")?;
Expand All @@ -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,
Expand Down
95 changes: 68 additions & 27 deletions src/services/telegram/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<DatabaseInterface>) -> 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<Bot, anyhow::Error, DefaultKey> {
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...");
Expand All @@ -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::<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()
Expand Down
12 changes: 12 additions & 0 deletions src/utils/italian.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use std::ops::Add;

pub fn countable_noun_suffix<T, U>(count: T, singular: &'static str, plural: &'static str)
-> &'static str
where
T: Default + Add<usize, Output=U> + PartialEq<U>,
{
match count == (T::default() + 1) {
true => singular,
false => plural,
}
}
1 change: 1 addition & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub mod time;
pub mod version;
pub mod anyhow_result;
pub mod telegram_string;
pub mod italian;
Loading