error handler wrapper helper

This commit is contained in:
Dylan Knutson
2025-09-02 19:55:15 +00:00
parent 8610b4cc52
commit c53dccbea2
14 changed files with 188 additions and 113 deletions

12
src/bot_result.rs Normal file
View File

@@ -0,0 +1,12 @@
use teloxide::dispatching::DpHandlerDescription;
#[derive(thiserror::Error, Debug)]
pub enum BotError {
#[error("User visible error: {0}")]
UserVisibleError(String),
#[error(transparent)]
InternalError(#[from] anyhow::Error),
}
pub type BotResult<T = ()> = Result<T, BotError>;
pub type BotHandler = dptree::Handler<'static, BotResult, DpHandlerDescription>;

View File

@@ -1,8 +1,10 @@
use teloxide::{prelude::*, types::Message, utils::command::BotCommands, Bot}; use crate::{
message_utils::{send_message, MessageTarget},
BotResult, Command,
};
use teloxide::{utils::command::BotCommands, Bot};
use crate::{BotResult, Command}; pub async fn handle_help(bot: Bot, target: MessageTarget) -> BotResult {
pub async fn handle_help(bot: Bot, msg: Message) -> BotResult {
let help_message = format!( let help_message = format!(
"📋 Available Commands:\n\n{}\n\n\ "📋 Available Commands:\n\n{}\n\n\
📧 Support: Contact @admin for help\n\ 📧 Support: Contact @admin for help\n\
@@ -10,6 +12,6 @@ pub async fn handle_help(bot: Bot, msg: Message) -> BotResult {
Command::descriptions() Command::descriptions()
); );
bot.send_message(msg.chat.id, help_message).await?; send_message(&bot, target, help_message, None).await?;
Ok(()) Ok(())
} }

View File

@@ -1,9 +1,11 @@
use crate::{
message_utils::{send_message, MessageTarget},
BotResult,
};
use log::info; use log::info;
use teloxide::{prelude::*, types::Message, Bot}; use teloxide::{types::Message, Bot};
use crate::BotResult; pub async fn handle_my_bids(bot: Bot, msg: Message, target: MessageTarget) -> BotResult {
pub async fn handle_my_bids(bot: Bot, msg: Message) -> BotResult {
let response = "🎯 My Bids (Coming Soon)\n\n\ let response = "🎯 My Bids (Coming Soon)\n\n\
Here you'll be able to view:\n\ Here you'll be able to view:\n\
• Your active bids\n\ • Your active bids\n\
@@ -18,6 +20,6 @@ pub async fn handle_my_bids(bot: Bot, msg: Message) -> BotResult {
msg.chat.id msg.chat.id
); );
bot.send_message(msg.chat.id, response).await?; send_message(&bot, target, response, None).await?;
Ok(()) Ok(())
} }

View File

@@ -20,8 +20,9 @@ use crate::{
find_or_create_db_user_from_message, message_into_message_target, find_or_create_db_user_from_message, message_into_message_target,
}, },
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget}, message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
BotResult, Command, DialogueRootState, RootDialogue, BotError, BotResult, Command, DialogueRootState, RootDialogue,
}; };
use anyhow::{anyhow, Context};
use log::info; use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -106,9 +107,15 @@ async fn handle_forward_listing(
) -> BotResult { ) -> BotResult {
info!("Handling forward listing inline query for listing {listing:?}"); info!("Handling forward listing inline query for listing {listing:?}");
let bot_username = match bot.get_me().await?.username.as_ref() { let bot_username = match bot
.get_me()
.await
.context("failed to get bot username")?
.username
.as_ref()
{
Some(username) => username.to_string(), Some(username) => username.to_string(),
None => anyhow::bail!("Bot username not found"), None => return Err(anyhow!("Bot username not found").into()),
}; };
// Create inline keyboard with auction interaction buttons // Create inline keyboard with auction interaction buttons
@@ -167,7 +174,8 @@ async fn handle_forward_listing(
.reply_markup(keyboard), // Add the inline keyboard here! .reply_markup(keyboard), // Add the inline keyboard here!
)], )],
) )
.await?; .await
.map_err(|e| anyhow::anyhow!("Error answering inline query: {e:?}"))?;
Ok(()) Ok(())
} }
@@ -215,7 +223,10 @@ pub async fn enter_my_listings(
flash: Option<String>, flash: Option<String>,
) -> BotResult { ) -> BotResult {
// Transition to ViewingListings state // Transition to ViewingListings state
dialogue.update(MyListingsState::ViewingListings).await?; dialogue
.update(MyListingsState::ViewingListings)
.await
.context("failed to update dialogue")?;
let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?; let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?;
// Create keyboard with buttons for each listing // Create keyboard with buttons for each listing
@@ -273,8 +284,7 @@ async fn handle_viewing_listings_callback(
let button = MyListingsButtons::try_from(data.as_str())?; let button = MyListingsButtons::try_from(data.as_str())?;
match button { match button {
MyListingsButtons::SelectListing(listing_id) => { MyListingsButtons::SelectListing(listing_id) => {
let listing = let listing = get_listing_for_user(&db_pool, user, listing_id).await?;
get_listing_for_user(&db_pool, &bot, user, listing_id, target.clone()).await?;
enter_show_listing_details(&bot, dialogue, listing, target).await?; enter_show_listing_details(&bot, dialogue, listing, target).await?;
} }
@@ -307,7 +317,8 @@ async fn enter_show_listing_details(
); );
dialogue dialogue
.update(MyListingsState::ManagingListing(listing_id)) .update(MyListingsState::ManagingListing(listing_id))
.await?; .await
.context("failed to update dialogue")?;
let keyboard = InlineKeyboardMarkup::default() let keyboard = InlineKeyboardMarkup::default()
.append_row([ .append_row([
ManageListingButtons::PreviewMessage.to_button(), ManageListingButtons::PreviewMessage.to_button(),
@@ -348,8 +359,7 @@ async fn handle_managing_listing_callback(
unimplemented!("Forward listing not implemented"); unimplemented!("Forward listing not implemented");
} }
ManageListingButtons::Edit => { ManageListingButtons::Edit => {
let listing = let listing = get_listing_for_user(&db_pool, user, listing_id).await?;
get_listing_for_user(&db_pool, &bot, user, listing_id, target.clone()).await?;
let draft = ListingDraft::from_persisted(listing); let draft = ListingDraft::from_persisted(listing);
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?; enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
} }
@@ -426,28 +436,20 @@ async fn send_preview_listing_message(
async fn get_listing_for_user( async fn get_listing_for_user(
db_pool: &SqlitePool, db_pool: &SqlitePool,
bot: &Bot,
user: PersistedUser, user: PersistedUser,
listing_id: ListingDbId, listing_id: ListingDbId,
target: MessageTarget,
) -> BotResult<PersistedListing> { ) -> BotResult<PersistedListing> {
let listing = match ListingDAO::find_by_id(db_pool, listing_id).await? { let listing = match ListingDAO::find_by_id(db_pool, listing_id).await? {
Some(listing) => listing, Some(listing) => listing,
None => { None => {
send_message(bot, target, "❌ Listing not found.", None).await?; return Err(BotError::UserVisibleError("❌ Listing not found.".into()));
return Err(anyhow::anyhow!("Listing not found"));
} }
}; };
if listing.base.seller_id != user.persisted.id { if listing.base.seller_id != user.persisted.id {
send_message( return Err(BotError::UserVisibleError(
bot, "❌ You can only manage your own auctions.".into(),
target, ));
"❌ You can only manage your own auctions.",
None,
)
.await?;
return Err(anyhow::anyhow!("User does not own listing"));
} }
Ok(listing) Ok(listing)

View File

@@ -261,7 +261,8 @@ async fn handle_starting_bid_amount_callback(
EditMinimumBidIncrementKeyboardButtons::OneDollar => "1.00", EditMinimumBidIncrementKeyboardButtons::OneDollar => "1.00",
EditMinimumBidIncrementKeyboardButtons::FiveDollars => "5.00", EditMinimumBidIncrementKeyboardButtons::FiveDollars => "5.00",
EditMinimumBidIncrementKeyboardButtons::TenDollars => "10.00", EditMinimumBidIncrementKeyboardButtons::TenDollars => "10.00",
})?; })
.map_err(|e| anyhow::anyhow!("Error parsing starting bid amount: {e:?}"))?;
update_field_on_draft( update_field_on_draft(
ListingField::StartingBidAmount, ListingField::StartingBidAmount,
@@ -307,11 +308,7 @@ async fn handle_currency_type_callback(
} }
/// Cancel the wizard and exit /// Cancel the wizard and exit
pub async fn cancel_wizard( pub async fn cancel_wizard(bot: Bot, dialogue: RootDialogue, target: MessageTarget) -> BotResult {
bot: Bot,
dialogue: RootDialogue,
target: MessageTarget,
) -> BotResult {
info!("{target:?} cancelled new listing wizard"); info!("{target:?} cancelled new listing wizard");
enter_select_new_listing_type(bot, dialogue, target).await?; enter_select_new_listing_type(bot, dialogue, target).await?;
Ok(()) Ok(())

View File

@@ -3,6 +3,8 @@
//! This module handles the core logic for processing and updating listing fields //! This module handles the core logic for processing and updating listing fields
//! during both initial creation and editing workflows. //! during both initial creation and editing workflows.
use anyhow::Context;
use crate::commands::new_listing::messages::step_for_field; use crate::commands::new_listing::messages::step_for_field;
use crate::commands::new_listing::{types::NewListingState, validations::*}; use crate::commands::new_listing::{types::NewListingState, validations::*};
use crate::{ use crate::{
@@ -18,7 +20,8 @@ pub async fn transition_to_field(
) -> BotResult { ) -> BotResult {
dialogue dialogue
.update(NewListingState::AwaitingDraftField { field, draft }) .update(NewListingState::AwaitingDraftField { field, draft })
.await?; .await
.context("failed to update dialogue")?;
Ok(()) Ok(())
} }

View File

@@ -28,9 +28,9 @@ use crate::{
ListingDAO, ListingDAO,
}, },
message_utils::*, message_utils::*,
DialogueRootState, BotResult, RootDialogue, BotResult, DialogueRootState, RootDialogue,
}; };
use anyhow::bail; use anyhow::{anyhow, Context};
use log::info; use log::info;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use teloxide::{prelude::*, types::*, Bot}; use teloxide::{prelude::*, types::*, Bot};
@@ -53,7 +53,8 @@ pub async fn enter_select_new_listing_type(
// Initialize the dialogue to listing type selection state // Initialize the dialogue to listing type selection state
dialogue dialogue
.update(NewListingState::SelectingListingType) .update(NewListingState::SelectingListingType)
.await?; .await
.context("failed to update dialogue")?;
send_message( send_message(
&bot, &bot,
@@ -83,10 +84,10 @@ pub async fn handle_awaiting_draft_field_input(
return Ok(()); return Ok(());
} }
Err(SetFieldError::UnsupportedFieldForListingType) => { Err(SetFieldError::UnsupportedFieldForListingType) => {
bail!("Cannot update field {field:?} for listing type"); return Err(anyhow!("Cannot update field {field:?} for listing type").into());
} }
Err(SetFieldError::FieldRequired) => { Err(SetFieldError::FieldRequired) => {
bail!("Cannot update field {field:?} on existing listing"); return Err(anyhow!("Cannot update field {field:?} on existing listing").into());
} }
}; };
@@ -124,10 +125,10 @@ pub async fn handle_editing_field_input(
return Ok(()); return Ok(());
} }
Err(SetFieldError::UnsupportedFieldForListingType) => { Err(SetFieldError::UnsupportedFieldForListingType) => {
bail!("Cannot update field {field:?} for listing type"); return Err(anyhow!("Cannot update field {field:?} for listing type").into());
} }
Err(SetFieldError::FieldRequired) => { Err(SetFieldError::FieldRequired) => {
bail!("Cannot update field {field:?} on existing listing"); return Err(anyhow!("Cannot update field {field:?} on existing listing").into());
} }
}; };
@@ -159,7 +160,7 @@ pub async fn handle_viewing_draft_callback(
let response = "🗑️ <b>Changes Discarded</b>\n\n\ let response = "🗑️ <b>Changes Discarded</b>\n\n\
Your changes have been discarded and not saved."; Your changes have been discarded and not saved.";
send_message(&bot, target, &response, None).await?; send_message(&bot, target, &response, None).await?;
dialogue.exit().await?; dialogue.exit().await.context("failed to exit dialogue")?;
} }
ConfirmationKeyboardButtons::Discard => { ConfirmationKeyboardButtons::Discard => {
info!("User {target:?} discarded listing creation"); info!("User {target:?} discarded listing creation");
@@ -168,7 +169,7 @@ pub async fn handle_viewing_draft_callback(
Your listing has been discarded and not created.\n\ Your listing has been discarded and not created.\n\
You can start a new listing anytime with /newlisting."; You can start a new listing anytime with /newlisting.";
send_message(&bot, target, &response, None).await?; send_message(&bot, target, &response, None).await?;
dialogue.exit().await?; dialogue.exit().await.context("failed to exit dialogue")?;
} }
ConfirmationKeyboardButtons::Edit => { ConfirmationKeyboardButtons::Edit => {
info!("User {target:?} chose to edit listing"); info!("User {target:?} chose to edit listing");
@@ -203,7 +204,7 @@ pub async fn handle_editing_draft_callback(
FieldSelectionKeyboardButtons::StartTime => ListingField::StartTime, FieldSelectionKeyboardButtons::StartTime => ListingField::StartTime,
FieldSelectionKeyboardButtons::Duration => ListingField::EndTime, FieldSelectionKeyboardButtons::Duration => ListingField::EndTime,
FieldSelectionKeyboardButtons::Done => { FieldSelectionKeyboardButtons::Done => {
return Err(anyhow::anyhow!("Done button should not be used here")) return Err(anyhow::anyhow!("Done button should not be used here").into());
} }
}; };
@@ -214,7 +215,8 @@ pub async fn handle_editing_draft_callback(
.update(DialogueRootState::NewListing( .update(DialogueRootState::NewListing(
NewListingState::EditingDraftField { field, draft }, NewListingState::EditingDraftField { field, draft },
)) ))
.await?; .await
.context("failed to update dialogue")?;
let response = format!("Editing {field:?}\n\nPrevious value: {value}"); let response = format!("Editing {field:?}\n\nPrevious value: {value}");
send_message(&bot, target, response, Some(keyboard)).await?; send_message(&bot, target, response, Some(keyboard)).await?;
@@ -263,7 +265,8 @@ pub async fn enter_edit_listing_draft(
.await?; .await?;
dialogue dialogue
.update(NewListingState::EditingDraft(draft)) .update(NewListingState::EditingDraft(draft))
.await?; .await
.context("failed to update dialogue")?;
Ok(()) Ok(())
} }

View File

@@ -9,6 +9,7 @@ use crate::commands::new_listing::NewListingState;
use crate::db::ListingType; use crate::db::ListingType;
use crate::RootDialogue; use crate::RootDialogue;
use crate::{commands::new_listing::types::ListingDraft, message_utils::*, BotResult}; use crate::{commands::new_listing::types::ListingDraft, message_utils::*, BotResult};
use anyhow::Context;
use teloxide::{types::InlineKeyboardMarkup, Bot}; use teloxide::{types::InlineKeyboardMarkup, Bot};
/// Display the listing summary with optional flash message and keyboard /// Display the listing summary with optional flash message and keyboard
@@ -77,6 +78,7 @@ pub async fn enter_confirm_save_listing(
display_listing_summary(bot, target, &draft, Some(keyboard), flash).await?; display_listing_summary(bot, target, &draft, Some(keyboard), flash).await?;
dialogue dialogue
.update(NewListingState::ViewingDraft(draft)) .update(NewListingState::ViewingDraft(draft))
.await?; .await
.context("failed to update dialogue")?;
Ok(()) Ok(())
} }

View File

@@ -1,9 +1,11 @@
use crate::{
message_utils::{send_message, MessageTarget},
BotResult,
};
use log::info; use log::info;
use teloxide::{prelude::*, types::Message, Bot}; use teloxide::{types::Message, Bot};
use crate::BotResult; pub async fn handle_settings(bot: Bot, msg: Message, target: MessageTarget) -> BotResult {
pub async fn handle_settings(bot: Bot, msg: Message) -> BotResult {
let response = "⚙️ Settings (Coming Soon)\n\n\ let response = "⚙️ Settings (Coming Soon)\n\n\
Here you'll be able to configure:\n\ Here you'll be able to configure:\n\
• Notification preferences\n\ • Notification preferences\n\
@@ -18,6 +20,6 @@ pub async fn handle_settings(bot: Bot, msg: Message) -> BotResult {
msg.chat.id msg.chat.id
); );
bot.send_message(msg.chat.id, response).await?; send_message(&bot, target, response, None).await?;
Ok(()) Ok(())
} }

View File

@@ -1,3 +1,4 @@
use anyhow::Context;
use log::info; use log::info;
use teloxide::{types::CallbackQuery, utils::command::BotCommands, Bot}; use teloxide::{types::CallbackQuery, utils::command::BotCommands, Bot};
@@ -8,7 +9,7 @@ use crate::{
db::user::PersistedUser, db::user::PersistedUser,
keyboard_buttons, keyboard_buttons,
message_utils::{extract_callback_data, send_message, MessageTarget}, message_utils::{extract_callback_data, send_message, MessageTarget},
Command, DialogueRootState, BotResult, RootDialogue, BotResult, Command, DialogueRootState, RootDialogue,
}; };
keyboard_buttons! { keyboard_buttons! {
@@ -35,22 +36,17 @@ pub fn get_main_menu_message() -> &'static str {
Choose an option below to get started! 🚀" Choose an option below to get started! 🚀"
} }
pub async fn handle_start( pub async fn handle_start(bot: Bot, dialogue: RootDialogue, target: MessageTarget) -> BotResult {
bot: Bot,
dialogue: RootDialogue,
target: MessageTarget,
) -> BotResult {
enter_main_menu(bot, dialogue, target).await?; enter_main_menu(bot, dialogue, target).await?;
Ok(()) Ok(())
} }
/// Show the main menu with buttons /// Show the main menu with buttons
pub async fn enter_main_menu( pub async fn enter_main_menu(bot: Bot, dialogue: RootDialogue, target: MessageTarget) -> BotResult {
bot: Bot, dialogue
dialogue: RootDialogue, .update(DialogueRootState::MainMenu)
target: MessageTarget, .await
) -> BotResult { .context("failed to update dialogue")?;
dialogue.update(DialogueRootState::MainMenu).await?;
send_message( send_message(
&bot, &bot,

47
src/handle_error.rs Normal file
View File

@@ -0,0 +1,47 @@
use crate::{
message_utils::{send_message, MessageTarget},
wrap_endpoint, BotError, BotResult, WrappedAsyncFn,
};
use futures::future::BoxFuture;
use std::{future::Future, pin::Pin};
use teloxide::Bot;
pub async fn handle_error(bot: Bot, target: MessageTarget, error: BotError) -> BotResult {
log::error!("Error in handler: {error}");
match error {
BotError::UserVisibleError(message) => send_message(&bot, target, message, None).await?,
BotError::InternalError(error) => {
log::error!("Internal error: {error}");
send_message(
&bot,
target,
"An internal error occurred. Please try again later.",
None,
)
.await?;
}
}
Ok(())
}
fn boxed_handle_error(
bot: Bot,
target: MessageTarget,
error: BotError,
) -> Pin<Box<dyn Future<Output = BotResult> + Send>> {
Box::pin(handle_error(bot, target, error))
}
pub type ErrorHandlerWrapped<FnBase, FnBaseArgs> = WrappedAsyncFn<
FnBase,
fn(Bot, MessageTarget, BotError) -> BoxFuture<'static, BotResult>,
BotError,
FnBaseArgs,
(Bot, MessageTarget),
>;
pub fn with_error_handler<FnBase, FnBaseArgs>(
handler: FnBase,
) -> ErrorHandlerWrapped<FnBase, FnBaseArgs> {
wrap_endpoint(handler, boxed_handle_error)
}

View File

@@ -1,15 +1,18 @@
mod bot_endpoint; mod bot_result;
mod commands; mod commands;
mod config; mod config;
mod db; mod db;
mod dptree_utils; mod dptree_utils;
mod handle_error;
mod handler_utils; mod handler_utils;
mod keyboard_utils; mod keyboard_utils;
mod message_utils; mod message_utils;
mod sqlite_storage; mod sqlite_storage;
#[cfg(test)] #[cfg(test)]
mod test_utils; mod test_utils;
mod wrap_endpoint;
use crate::handle_error::with_error_handler;
use crate::handler_utils::{callback_query_into_message_target, message_into_message_target}; use crate::handler_utils::{callback_query_into_message_target, message_into_message_target};
use crate::sqlite_storage::SqliteStorage; use crate::sqlite_storage::SqliteStorage;
use crate::{ use crate::{
@@ -20,13 +23,14 @@ use crate::{
handler_utils::find_or_create_db_user_from_callback_query, handler_utils::find_or_create_db_user_from_callback_query,
}; };
use anyhow::Result; use anyhow::Result;
pub use bot_endpoint::*; pub use bot_result::*;
use commands::*; use commands::*;
use config::Config; use config::Config;
use log::info; use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use teloxide::dispatching::dialogue::serializer::Json; use teloxide::dispatching::dialogue::serializer::Json;
use teloxide::{prelude::*, types::BotCommand, utils::command::BotCommands}; use teloxide::{prelude::*, types::BotCommand, utils::command::BotCommands};
pub use wrap_endpoint::*;
/// Set up the bot's command menu that appears when users tap the menu button /// Set up the bot's command menu that appears when users tap the menu button
async fn setup_bot_commands(bot: &Bot) -> Result<()> { async fn setup_bot_commands(bot: &Bot) -> Result<()> {
@@ -73,11 +77,6 @@ enum DialogueRootState {
type RootDialogue = Dialogue<DialogueRootState, SqliteStorage<Json>>; type RootDialogue = Dialogue<DialogueRootState, SqliteStorage<Json>>;
async fn handle_error(bot: Bot, error: BotError) -> BotResult {
log::error!("Error in handler: {error}");
Ok(())
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
// Load and validate configuration from environment/.env file // Load and validate configuration from environment/.env file
@@ -110,23 +109,36 @@ async fn main() -> Result<()> {
.branch( .branch(
dptree::case![DialogueRootState::MainMenu] dptree::case![DialogueRootState::MainMenu]
.filter_map_async(find_or_create_db_user_from_callback_query) .filter_map_async(find_or_create_db_user_from_callback_query)
.endpoint(wrap_endpoint( .endpoint(with_error_handler(handle_main_menu_callback)),
handle_main_menu_callback,
handle_error,
)),
), ),
) )
.branch( .branch(
Update::filter_message() Update::filter_message()
.map(message_into_message_target) .map(message_into_message_target)
.filter_command::<Command>() .filter_command::<Command>()
.branch(dptree::case![Command::Start].endpoint(handle_start)) .branch(
.branch(dptree::case![Command::Help].endpoint(handle_help)) dptree::case![Command::Start]
.branch(dptree::case![Command::MyBids].endpoint(handle_my_bids)) .endpoint(with_error_handler(handle_start)),
.branch(dptree::case![Command::Settings].endpoint(handle_settings)), )
.branch(
dptree::case![Command::Help]
.endpoint(with_error_handler(handle_help)),
)
.branch(
dptree::case![Command::MyBids]
.endpoint(with_error_handler(handle_my_bids)),
)
.branch(
dptree::case![Command::Settings]
.endpoint(with_error_handler(handle_settings)),
),
), ),
) )
.branch(Update::filter_message().endpoint(unknown_message_handler)), .branch(
Update::filter_message()
.map(message_into_message_target)
.endpoint(with_error_handler(unknown_message_handler)),
),
) )
.dependencies(dptree::deps![db_pool, dialog_storage]) .dependencies(dptree::deps![db_pool, dialog_storage])
.enable_ctrlc_handler() .enable_ctrlc_handler()
@@ -138,17 +150,11 @@ async fn main() -> Result<()> {
Ok(()) Ok(())
} }
async fn unknown_message_handler(bot: Bot, msg: Message) -> BotResult { async fn unknown_message_handler(msg: Message) -> BotResult {
bot.send_message( Err(BotError::UserVisibleError(format!(
msg.chat.id, "Unknown command: `{}`\n\n\
format!( Try /help to see the list of commands.\
" ",
Unknown command: `{}`\n\n\ msg.text().unwrap_or("")
Try /help to see the list of commands.\ )))
",
msg.text().unwrap_or("")
),
)
.await?;
Ok(())
} }

View File

@@ -1,5 +1,5 @@
use crate::BotResult; use crate::BotResult;
use anyhow::bail; use anyhow::{anyhow, Context};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use num::One; use num::One;
use std::fmt::Display; use std::fmt::Display;
@@ -126,7 +126,7 @@ pub async fn send_message(
if let Some(kb) = keyboard { if let Some(kb) = keyboard {
message = message.reply_markup(kb); message = message.reply_markup(kb);
} }
message.await?; message.await.context("failed to edit message")?;
} else { } else {
let mut message = bot let mut message = bot
.send_message(target.chat_id, text.as_ref()) .send_message(target.chat_id, text.as_ref())
@@ -134,7 +134,7 @@ pub async fn send_message(
if let Some(kb) = keyboard { if let Some(kb) = keyboard {
message = message.reply_markup(kb); message = message.reply_markup(kb);
} }
message.await?; message.await.context("failed to send message")?;
} }
Ok(()) Ok(())
} }
@@ -152,7 +152,7 @@ pub fn create_single_button_keyboard(text: &str, callback_data: &str) -> InlineK
pub async fn extract_callback_data(bot: &Bot, callback_query: CallbackQuery) -> BotResult<String> { pub async fn extract_callback_data(bot: &Bot, callback_query: CallbackQuery) -> BotResult<String> {
let data = match callback_query.data { let data = match callback_query.data {
Some(data) => data, Some(data) => data,
None => bail!("Missing data in callback query"), None => return Err(anyhow!("Missing data in callback query"))?,
}; };
// Answer the callback query to remove loading state // Answer the callback query to remove loading state

View File

@@ -2,11 +2,6 @@ use dptree::di::{CompiledFn, DependencyMap, Injectable};
use dptree::Type; use dptree::Type;
use std::sync::Arc; use std::sync::Arc;
use std::{collections::BTreeSet, future::Future, marker::PhantomData}; use std::{collections::BTreeSet, future::Future, marker::PhantomData};
use teloxide::dispatching::DpHandlerDescription;
pub type BotError = anyhow::Error;
pub type BotResult<T = ()> = Result<T, BotError>;
pub type BotHandler = dptree::Handler<'static, BotResult, DpHandlerDescription>;
pub struct WrappedAsyncFn<FnBase, FnError, ErrorType, FnBaseArgs, FnErrorArgs> { pub struct WrappedAsyncFn<FnBase, FnError, ErrorType, FnBaseArgs, FnErrorArgs> {
fn_base: FnBase, fn_base: FnBase,
@@ -89,7 +84,15 @@ generate_wrapped!([T1, T2, T3, T4], [E1]);
generate_wrapped!([T1, T2, T3, T4, T5], [E1]); generate_wrapped!([T1, T2, T3, T4, T5], [E1]);
generate_wrapped!([T1, T2, T3, T4, T5, T6], [E1]); generate_wrapped!([T1, T2, T3, T4, T5, T6], [E1]);
pub fn wrap_endpoint<FnError, FnBase, ErrorType, FnBaseArgs, FnErrorArgs>( generate_wrapped!([], [E1, E2]);
generate_wrapped!([T1], [E1, E2]);
generate_wrapped!([T1, T2], [E1, E2]);
generate_wrapped!([T1, T2, T3], [E1, E2]);
generate_wrapped!([T1, T2, T3, T4], [E1, E2]);
generate_wrapped!([T1, T2, T3, T4, T5], [E1, E2]);
generate_wrapped!([T1, T2, T3, T4, T5, T6], [E1, E2]);
pub fn wrap_endpoint<FnBase, FnError, ErrorType, FnBaseArgs, FnErrorArgs>(
fn_base: FnBase, fn_base: FnBase,
error_fn: FnError, error_fn: FnError,
) -> WrappedAsyncFn<FnBase, FnError, ErrorType, FnBaseArgs, FnErrorArgs> { ) -> WrappedAsyncFn<FnBase, FnError, ErrorType, FnBaseArgs, FnErrorArgs> {
@@ -98,12 +101,10 @@ pub fn wrap_endpoint<FnError, FnBase, ErrorType, FnBaseArgs, FnErrorArgs>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::ops::ControlFlow;
use dptree::{deps, Handler};
use teloxide::dispatching::DpHandlerDescription;
use crate::wrap_endpoint; use crate::wrap_endpoint;
use dptree::{deps, Handler};
use std::ops::ControlFlow;
use teloxide::dispatching::DpHandlerDescription;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
enum MyError { enum MyError {