From c53dccbea2e171f116564056e522bcc4ac6d0517 Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Tue, 2 Sep 2025 19:55:15 +0000 Subject: [PATCH] error handler wrapper helper --- src/bot_result.rs | 12 ++++ src/commands/help.rs | 12 ++-- src/commands/my_bids.rs | 12 ++-- src/commands/my_listings/mod.rs | 46 +++++++------- src/commands/new_listing/callbacks.rs | 9 +-- src/commands/new_listing/field_processing.rs | 5 +- src/commands/new_listing/handlers.rs | 27 +++++---- src/commands/new_listing/ui.rs | 4 +- src/commands/settings.rs | 12 ++-- src/commands/start.rs | 20 +++--- src/handle_error.rs | 47 ++++++++++++++ src/main.rs | 64 +++++++++++--------- src/message_utils.rs | 8 +-- src/{bot_endpoint.rs => wrap_endpoint.rs} | 23 +++---- 14 files changed, 188 insertions(+), 113 deletions(-) create mode 100644 src/bot_result.rs create mode 100644 src/handle_error.rs rename src/{bot_endpoint.rs => wrap_endpoint.rs} (92%) diff --git a/src/bot_result.rs b/src/bot_result.rs new file mode 100644 index 0000000..e78e1d7 --- /dev/null +++ b/src/bot_result.rs @@ -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 = Result; +pub type BotHandler = dptree::Handler<'static, BotResult, DpHandlerDescription>; diff --git a/src/commands/help.rs b/src/commands/help.rs index 5adf490..0f9455a 100644 --- a/src/commands/help.rs +++ b/src/commands/help.rs @@ -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, msg: Message) -> BotResult { +pub async fn handle_help(bot: Bot, target: MessageTarget) -> BotResult { let help_message = format!( "📋 Available Commands:\n\n{}\n\n\ 📧 Support: Contact @admin for help\n\ @@ -10,6 +12,6 @@ pub async fn handle_help(bot: Bot, msg: Message) -> BotResult { Command::descriptions() ); - bot.send_message(msg.chat.id, help_message).await?; + send_message(&bot, target, help_message, None).await?; Ok(()) } diff --git a/src/commands/my_bids.rs b/src/commands/my_bids.rs index 5add304..39d08d3 100644 --- a/src/commands/my_bids.rs +++ b/src/commands/my_bids.rs @@ -1,9 +1,11 @@ +use crate::{ + message_utils::{send_message, MessageTarget}, + BotResult, +}; 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) -> BotResult { +pub async fn handle_my_bids(bot: Bot, msg: Message, target: MessageTarget) -> BotResult { let response = "🎯 My Bids (Coming Soon)\n\n\ Here you'll be able to view:\n\ • Your active bids\n\ @@ -18,6 +20,6 @@ pub async fn handle_my_bids(bot: Bot, msg: Message) -> BotResult { msg.chat.id ); - bot.send_message(msg.chat.id, response).await?; + send_message(&bot, target, response, None).await?; Ok(()) } diff --git a/src/commands/my_listings/mod.rs b/src/commands/my_listings/mod.rs index cd2d762..c80c52f 100644 --- a/src/commands/my_listings/mod.rs +++ b/src/commands/my_listings/mod.rs @@ -20,8 +20,9 @@ use crate::{ find_or_create_db_user_from_message, message_into_message_target, }, 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 serde::{Deserialize, Serialize}; use sqlx::SqlitePool; @@ -106,9 +107,15 @@ async fn handle_forward_listing( ) -> BotResult { 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(), - None => anyhow::bail!("Bot username not found"), + None => return Err(anyhow!("Bot username not found").into()), }; // Create inline keyboard with auction interaction buttons @@ -167,7 +174,8 @@ async fn handle_forward_listing( .reply_markup(keyboard), // Add the inline keyboard here! )], ) - .await?; + .await + .map_err(|e| anyhow::anyhow!("Error answering inline query: {e:?}"))?; Ok(()) } @@ -215,7 +223,10 @@ pub async fn enter_my_listings( flash: Option, ) -> BotResult { // 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?; // 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())?; match button { MyListingsButtons::SelectListing(listing_id) => { - let listing = - get_listing_for_user(&db_pool, &bot, user, listing_id, target.clone()).await?; + let listing = get_listing_for_user(&db_pool, user, listing_id).await?; enter_show_listing_details(&bot, dialogue, listing, target).await?; } @@ -307,7 +317,8 @@ async fn enter_show_listing_details( ); dialogue .update(MyListingsState::ManagingListing(listing_id)) - .await?; + .await + .context("failed to update dialogue")?; let keyboard = InlineKeyboardMarkup::default() .append_row([ ManageListingButtons::PreviewMessage.to_button(), @@ -348,8 +359,7 @@ async fn handle_managing_listing_callback( unimplemented!("Forward listing not implemented"); } ManageListingButtons::Edit => { - let listing = - get_listing_for_user(&db_pool, &bot, user, listing_id, target.clone()).await?; + let listing = get_listing_for_user(&db_pool, user, listing_id).await?; let draft = ListingDraft::from_persisted(listing); 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( db_pool: &SqlitePool, - bot: &Bot, user: PersistedUser, listing_id: ListingDbId, - target: MessageTarget, ) -> BotResult { let listing = match ListingDAO::find_by_id(db_pool, listing_id).await? { Some(listing) => listing, None => { - send_message(bot, target, "❌ Listing not found.", None).await?; - return Err(anyhow::anyhow!("Listing not found")); + return Err(BotError::UserVisibleError("❌ Listing not found.".into())); } }; if listing.base.seller_id != user.persisted.id { - send_message( - bot, - target, - "❌ You can only manage your own auctions.", - None, - ) - .await?; - return Err(anyhow::anyhow!("User does not own listing")); + return Err(BotError::UserVisibleError( + "❌ You can only manage your own auctions.".into(), + )); } Ok(listing) diff --git a/src/commands/new_listing/callbacks.rs b/src/commands/new_listing/callbacks.rs index e5ecdcf..b3f57e7 100644 --- a/src/commands/new_listing/callbacks.rs +++ b/src/commands/new_listing/callbacks.rs @@ -261,7 +261,8 @@ async fn handle_starting_bid_amount_callback( EditMinimumBidIncrementKeyboardButtons::OneDollar => "1.00", EditMinimumBidIncrementKeyboardButtons::FiveDollars => "5.00", EditMinimumBidIncrementKeyboardButtons::TenDollars => "10.00", - })?; + }) + .map_err(|e| anyhow::anyhow!("Error parsing starting bid amount: {e:?}"))?; update_field_on_draft( ListingField::StartingBidAmount, @@ -307,11 +308,7 @@ async fn handle_currency_type_callback( } /// Cancel the wizard and exit -pub async fn cancel_wizard( - bot: Bot, - dialogue: RootDialogue, - target: MessageTarget, -) -> BotResult { +pub async fn cancel_wizard(bot: Bot, dialogue: RootDialogue, target: MessageTarget) -> BotResult { info!("{target:?} cancelled new listing wizard"); enter_select_new_listing_type(bot, dialogue, target).await?; Ok(()) diff --git a/src/commands/new_listing/field_processing.rs b/src/commands/new_listing/field_processing.rs index f5e4f8f..b66c624 100644 --- a/src/commands/new_listing/field_processing.rs +++ b/src/commands/new_listing/field_processing.rs @@ -3,6 +3,8 @@ //! This module handles the core logic for processing and updating listing fields //! during both initial creation and editing workflows. +use anyhow::Context; + use crate::commands::new_listing::messages::step_for_field; use crate::commands::new_listing::{types::NewListingState, validations::*}; use crate::{ @@ -18,7 +20,8 @@ pub async fn transition_to_field( ) -> BotResult { dialogue .update(NewListingState::AwaitingDraftField { field, draft }) - .await?; + .await + .context("failed to update dialogue")?; Ok(()) } diff --git a/src/commands/new_listing/handlers.rs b/src/commands/new_listing/handlers.rs index 691bbb5..0156a26 100644 --- a/src/commands/new_listing/handlers.rs +++ b/src/commands/new_listing/handlers.rs @@ -28,9 +28,9 @@ use crate::{ ListingDAO, }, message_utils::*, - DialogueRootState, BotResult, RootDialogue, + BotResult, DialogueRootState, RootDialogue, }; -use anyhow::bail; +use anyhow::{anyhow, Context}; use log::info; use sqlx::SqlitePool; 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 dialogue .update(NewListingState::SelectingListingType) - .await?; + .await + .context("failed to update dialogue")?; send_message( &bot, @@ -83,10 +84,10 @@ pub async fn handle_awaiting_draft_field_input( return Ok(()); } 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) => { - 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(()); } 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) => { - 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 = "🗑️ Changes Discarded\n\n\ Your changes have been discarded and not saved."; send_message(&bot, target, &response, None).await?; - dialogue.exit().await?; + dialogue.exit().await.context("failed to exit dialogue")?; } ConfirmationKeyboardButtons::Discard => { 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\ You can start a new listing anytime with /newlisting."; send_message(&bot, target, &response, None).await?; - dialogue.exit().await?; + dialogue.exit().await.context("failed to exit dialogue")?; } ConfirmationKeyboardButtons::Edit => { info!("User {target:?} chose to edit listing"); @@ -203,7 +204,7 @@ pub async fn handle_editing_draft_callback( FieldSelectionKeyboardButtons::StartTime => ListingField::StartTime, FieldSelectionKeyboardButtons::Duration => ListingField::EndTime, 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( NewListingState::EditingDraftField { field, draft }, )) - .await?; + .await + .context("failed to update dialogue")?; let response = format!("Editing {field:?}\n\nPrevious value: {value}"); send_message(&bot, target, response, Some(keyboard)).await?; @@ -263,7 +265,8 @@ pub async fn enter_edit_listing_draft( .await?; dialogue .update(NewListingState::EditingDraft(draft)) - .await?; + .await + .context("failed to update dialogue")?; Ok(()) } diff --git a/src/commands/new_listing/ui.rs b/src/commands/new_listing/ui.rs index 7e9c729..b232a60 100644 --- a/src/commands/new_listing/ui.rs +++ b/src/commands/new_listing/ui.rs @@ -9,6 +9,7 @@ use crate::commands::new_listing::NewListingState; use crate::db::ListingType; use crate::RootDialogue; use crate::{commands::new_listing::types::ListingDraft, message_utils::*, BotResult}; +use anyhow::Context; use teloxide::{types::InlineKeyboardMarkup, Bot}; /// 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?; dialogue .update(NewListingState::ViewingDraft(draft)) - .await?; + .await + .context("failed to update dialogue")?; Ok(()) } diff --git a/src/commands/settings.rs b/src/commands/settings.rs index 51f02d0..328757e 100644 --- a/src/commands/settings.rs +++ b/src/commands/settings.rs @@ -1,9 +1,11 @@ +use crate::{ + message_utils::{send_message, MessageTarget}, + BotResult, +}; 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) -> BotResult { +pub async fn handle_settings(bot: Bot, msg: Message, target: MessageTarget) -> BotResult { let response = "⚙️ Settings (Coming Soon)\n\n\ Here you'll be able to configure:\n\ • Notification preferences\n\ @@ -18,6 +20,6 @@ pub async fn handle_settings(bot: Bot, msg: Message) -> BotResult { msg.chat.id ); - bot.send_message(msg.chat.id, response).await?; + send_message(&bot, target, response, None).await?; Ok(()) } diff --git a/src/commands/start.rs b/src/commands/start.rs index e921c1f..557eb11 100644 --- a/src/commands/start.rs +++ b/src/commands/start.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use log::info; use teloxide::{types::CallbackQuery, utils::command::BotCommands, Bot}; @@ -8,7 +9,7 @@ use crate::{ db::user::PersistedUser, keyboard_buttons, message_utils::{extract_callback_data, send_message, MessageTarget}, - Command, DialogueRootState, BotResult, RootDialogue, + BotResult, Command, DialogueRootState, RootDialogue, }; keyboard_buttons! { @@ -35,22 +36,17 @@ pub fn get_main_menu_message() -> &'static str { Choose an option below to get started! 🚀" } -pub async fn handle_start( - bot: Bot, - dialogue: RootDialogue, - target: MessageTarget, -) -> BotResult { +pub async fn handle_start(bot: Bot, dialogue: RootDialogue, target: MessageTarget) -> BotResult { enter_main_menu(bot, dialogue, target).await?; Ok(()) } /// Show the main menu with buttons -pub async fn enter_main_menu( - bot: Bot, - dialogue: RootDialogue, - target: MessageTarget, -) -> BotResult { - dialogue.update(DialogueRootState::MainMenu).await?; +pub async fn enter_main_menu(bot: Bot, dialogue: RootDialogue, target: MessageTarget) -> BotResult { + dialogue + .update(DialogueRootState::MainMenu) + .await + .context("failed to update dialogue")?; send_message( &bot, diff --git a/src/handle_error.rs b/src/handle_error.rs new file mode 100644 index 0000000..a351659 --- /dev/null +++ b/src/handle_error.rs @@ -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 + Send>> { + Box::pin(handle_error(bot, target, error)) +} + +pub type ErrorHandlerWrapped = WrappedAsyncFn< + FnBase, + fn(Bot, MessageTarget, BotError) -> BoxFuture<'static, BotResult>, + BotError, + FnBaseArgs, + (Bot, MessageTarget), +>; + +pub fn with_error_handler( + handler: FnBase, +) -> ErrorHandlerWrapped { + wrap_endpoint(handler, boxed_handle_error) +} diff --git a/src/main.rs b/src/main.rs index 9d00996..3dd5e3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,18 @@ -mod bot_endpoint; +mod bot_result; mod commands; mod config; mod db; mod dptree_utils; +mod handle_error; mod handler_utils; mod keyboard_utils; mod message_utils; mod sqlite_storage; #[cfg(test)] 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::sqlite_storage::SqliteStorage; use crate::{ @@ -20,13 +23,14 @@ use crate::{ handler_utils::find_or_create_db_user_from_callback_query, }; use anyhow::Result; -pub use bot_endpoint::*; +pub use bot_result::*; use commands::*; use config::Config; use log::info; use serde::{Deserialize, Serialize}; use teloxide::dispatching::dialogue::serializer::Json; 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 async fn setup_bot_commands(bot: &Bot) -> Result<()> { @@ -73,11 +77,6 @@ enum DialogueRootState { type RootDialogue = Dialogue>; -async fn handle_error(bot: Bot, error: BotError) -> BotResult { - log::error!("Error in handler: {error}"); - Ok(()) -} - #[tokio::main] async fn main() -> Result<()> { // Load and validate configuration from environment/.env file @@ -110,23 +109,36 @@ async fn main() -> Result<()> { .branch( dptree::case![DialogueRootState::MainMenu] .filter_map_async(find_or_create_db_user_from_callback_query) - .endpoint(wrap_endpoint( - handle_main_menu_callback, - handle_error, - )), + .endpoint(with_error_handler(handle_main_menu_callback)), ), ) .branch( Update::filter_message() .map(message_into_message_target) .filter_command::() - .branch(dptree::case![Command::Start].endpoint(handle_start)) - .branch(dptree::case![Command::Help].endpoint(handle_help)) - .branch(dptree::case![Command::MyBids].endpoint(handle_my_bids)) - .branch(dptree::case![Command::Settings].endpoint(handle_settings)), + .branch( + dptree::case![Command::Start] + .endpoint(with_error_handler(handle_start)), + ) + .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]) .enable_ctrlc_handler() @@ -138,17 +150,11 @@ async fn main() -> Result<()> { Ok(()) } -async fn unknown_message_handler(bot: Bot, msg: Message) -> BotResult { - bot.send_message( - msg.chat.id, - format!( - " - Unknown command: `{}`\n\n\ - Try /help to see the list of commands.\ - ", - msg.text().unwrap_or("") - ), - ) - .await?; - Ok(()) +async fn unknown_message_handler(msg: Message) -> BotResult { + Err(BotError::UserVisibleError(format!( + "Unknown command: `{}`\n\n\ + Try /help to see the list of commands.\ + ", + msg.text().unwrap_or("") + ))) } diff --git a/src/message_utils.rs b/src/message_utils.rs index 060aa66..39e6a76 100644 --- a/src/message_utils.rs +++ b/src/message_utils.rs @@ -1,5 +1,5 @@ use crate::BotResult; -use anyhow::bail; +use anyhow::{anyhow, Context}; use chrono::{DateTime, Utc}; use num::One; use std::fmt::Display; @@ -126,7 +126,7 @@ pub async fn send_message( if let Some(kb) = keyboard { message = message.reply_markup(kb); } - message.await?; + message.await.context("failed to edit message")?; } else { let mut message = bot .send_message(target.chat_id, text.as_ref()) @@ -134,7 +134,7 @@ pub async fn send_message( if let Some(kb) = keyboard { message = message.reply_markup(kb); } - message.await?; + message.await.context("failed to send message")?; } 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 { let data = match callback_query.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 diff --git a/src/bot_endpoint.rs b/src/wrap_endpoint.rs similarity index 92% rename from src/bot_endpoint.rs rename to src/wrap_endpoint.rs index 0b3e65d..00cbf21 100644 --- a/src/bot_endpoint.rs +++ b/src/wrap_endpoint.rs @@ -2,11 +2,6 @@ use dptree::di::{CompiledFn, DependencyMap, Injectable}; use dptree::Type; use std::sync::Arc; use std::{collections::BTreeSet, future::Future, marker::PhantomData}; -use teloxide::dispatching::DpHandlerDescription; - -pub type BotError = anyhow::Error; -pub type BotResult = Result; -pub type BotHandler = dptree::Handler<'static, BotResult, DpHandlerDescription>; pub struct WrappedAsyncFn { 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, T6], [E1]); -pub fn wrap_endpoint( +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( fn_base: FnBase, error_fn: FnError, ) -> WrappedAsyncFn { @@ -98,12 +101,10 @@ pub fn wrap_endpoint( #[cfg(test)] mod tests { - use std::ops::ControlFlow; - - use dptree::{deps, Handler}; - use teloxide::dispatching::DpHandlerDescription; - use crate::wrap_endpoint; + use dptree::{deps, Handler}; + use std::ops::ControlFlow; + use teloxide::dispatching::DpHandlerDescription; #[derive(Debug, PartialEq, Eq)] enum MyError {