From 764c17af0563d80bdd7e27fc6dd18fb922212080 Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Thu, 28 Aug 2025 07:23:40 +0000 Subject: [PATCH] Fix dialogue handler structure and enhance duration input - Fix handler type mismatch error by properly ordering dialogue entry - Move .enter_dialogue() before handlers that need dialogue context - Remove duplicate command handler branches - Add duration callback handler for inline keyboard buttons - Add duration keyboard with 1, 3, 7, and 14 day options - Refactor duration processing into shared function - Simplify slots keyboard layout to single row - Improve code organization and error handling --- src/commands/mod.rs | 3 - src/commands/new_listing.rs | 308 +++++++++++++++++++++++------------- src/main.rs | 34 ++-- 3 files changed, 213 insertions(+), 132 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ae05ddf..5460e8f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,9 +9,6 @@ pub mod start; pub use help::handle_help; pub use my_bids::handle_my_bids; pub use my_listings::handle_my_listings; -pub use new_listing::{ - handle_new_listing, new_listing_callback_handler, new_listing_dialogue_handler, -}; pub use settings::handle_settings; pub use start::handle_start; diff --git a/src/commands/new_listing.rs b/src/commands/new_listing.rs index ecd4b88..5af88ff 100644 --- a/src/commands/new_listing.rs +++ b/src/commands/new_listing.rs @@ -20,7 +20,7 @@ use crate::{ }, message_utils::{is_cancel, is_cancel_or_no, UserHandleAndId}, sqlite_storage::SqliteStorage, - HandlerResult, + Command, HandlerResult, }; #[derive(Clone, Serialize, Deserialize, Default)] @@ -55,59 +55,131 @@ pub enum ListingWizardState { } // Type alias for the dialogue -pub type NewListingDialogue = Dialogue>; +type NewListingDialogue = Dialogue>; // Create the dialogue handler tree for new listing wizard -pub fn new_listing_dialogue_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> { +pub fn new_listing_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> { dptree::entry() - .branch(dptree::case![ListingWizardState::Start].endpoint(start_new_listing)) .branch( - dptree::case![ListingWizardState::AwaitingTitle(state)].endpoint(handle_title_input), + Update::filter_message() + .enter_dialogue::, ListingWizardState>() + .branch(dptree::entry().filter_command::().branch( + dptree::case![Command::NewListing].endpoint(handle_new_listing_command), + )) + .branch(dptree::case![ListingWizardState::Start].endpoint(start_new_listing)) + .branch( + dptree::case![ListingWizardState::AwaitingTitle(state)] + .endpoint(handle_title_input), + ) + .branch( + dptree::case![ListingWizardState::AwaitingDescription(state)] + .endpoint(handle_description_input), + ) + .branch( + dptree::case![ListingWizardState::AwaitingPrice(state)] + .endpoint(handle_price_input), + ) + .branch( + dptree::case![ListingWizardState::AwaitingSlots(state)] + .endpoint(handle_slots_input), + ) + .branch( + dptree::case![ListingWizardState::AwaitingStartTime(state)] + .endpoint(handle_start_time_input), + ) + .branch( + dptree::case![ListingWizardState::AwaitingDuration(state)] + .endpoint(handle_duration_input), + ) + .branch( + dptree::case![ListingWizardState::ViewingDraft(state)] + .endpoint(handle_viewing_draft), + ) + .branch( + dptree::case![ListingWizardState::EditingListing(state)] + .endpoint(handle_editing_screen), + ) + .branch( + dptree::case![ListingWizardState::EditingTitle(state)] + .endpoint(handle_edit_title), + ) + .branch( + dptree::case![ListingWizardState::EditingDescription(state)] + .endpoint(handle_edit_description), + ) + .branch( + dptree::case![ListingWizardState::EditingPrice(state)] + .endpoint(handle_edit_price), + ) + .branch( + dptree::case![ListingWizardState::EditingSlots(state)] + .endpoint(handle_edit_slots), + ) + .branch( + dptree::case![ListingWizardState::EditingStartTime(state)] + .endpoint(handle_edit_start_time), + ) + .branch( + dptree::case![ListingWizardState::EditingDuration(state)] + .endpoint(handle_edit_duration), + ), ) .branch( - dptree::case![ListingWizardState::AwaitingDescription(state)] - .endpoint(handle_description_input), - ) - .branch( - dptree::case![ListingWizardState::AwaitingPrice(state)].endpoint(handle_price_input), - ) - .branch( - dptree::case![ListingWizardState::AwaitingSlots(state)].endpoint(handle_slots_input), - ) - .branch( - dptree::case![ListingWizardState::AwaitingStartTime(state)] - .endpoint(handle_start_time_input), - ) - .branch( - dptree::case![ListingWizardState::AwaitingDuration(state)] - .endpoint(handle_duration_input), - ) - .branch( - dptree::case![ListingWizardState::ViewingDraft(state)].endpoint(handle_viewing_draft), - ) - .branch( - dptree::case![ListingWizardState::EditingListing(state)] - .endpoint(handle_editing_screen), - ) - .branch(dptree::case![ListingWizardState::EditingTitle(state)].endpoint(handle_edit_title)) - .branch( - dptree::case![ListingWizardState::EditingDescription(state)] - .endpoint(handle_edit_description), - ) - .branch(dptree::case![ListingWizardState::EditingPrice(state)].endpoint(handle_edit_price)) - .branch(dptree::case![ListingWizardState::EditingSlots(state)].endpoint(handle_edit_slots)) - .branch( - dptree::case![ListingWizardState::EditingStartTime(state)] - .endpoint(handle_edit_start_time), - ) - .branch( - dptree::case![ListingWizardState::EditingDuration(state)] - .endpoint(handle_edit_duration), + Update::filter_callback_query() + .enter_dialogue::, ListingWizardState>() + .branch( + dptree::case![ListingWizardState::AwaitingDescription(state)] + .endpoint(handle_description_callback), + ) + .branch( + dptree::case![ListingWizardState::AwaitingSlots(state)] + .endpoint(handle_slots_callback), + ) + .branch( + dptree::case![ListingWizardState::AwaitingStartTime(state)] + .endpoint(handle_start_time_callback), + ) + .branch( + dptree::case![ListingWizardState::AwaitingDuration(state)] + .endpoint(handle_duration_callback), + ) + .branch( + dptree::case![ListingWizardState::ViewingDraft(state)] + .endpoint(handle_viewing_draft_callback), + ) + .branch( + dptree::case![ListingWizardState::EditingListing(state)] + .endpoint(handle_editing_callback), + ) + .branch( + dptree::case![ListingWizardState::EditingTitle(state)] + .endpoint(handle_edit_field_callback), + ) + .branch( + dptree::case![ListingWizardState::EditingDescription(state)] + .endpoint(handle_edit_field_callback), + ) + .branch( + dptree::case![ListingWizardState::EditingPrice(state)] + .endpoint(handle_edit_field_callback), + ) + .branch( + dptree::case![ListingWizardState::EditingSlots(state)] + .endpoint(handle_edit_field_callback), + ) + .branch( + dptree::case![ListingWizardState::EditingStartTime(state)] + .endpoint(handle_edit_field_callback), + ) + .branch( + dptree::case![ListingWizardState::EditingDuration(state)] + .endpoint(handle_edit_field_callback), + ), ) } -// Handle the /newlisting command - starts the dialogue -pub async fn handle_new_listing( +// Handle the /newlisting command - starts the dialogue by setting it to Start state +async fn handle_new_listing_command( bot: Bot, dialogue: NewListingDialogue, msg: Message, @@ -118,6 +190,28 @@ pub async fn handle_new_listing( msg.chat.id ); + // Initialize the dialogue to Start state + dialogue.update(ListingWizardState::Start).await?; + + let response = "🛍️ Creating New Fixed Price Listing\n\n\ + Let's create your fixed price listing step by step!\n\n\ + Step 1 of 6: Title\n\ + Please enter a title for your listing (max 100 characters):"; + + bot.send_message(msg.chat.id, response) + .parse_mode(ParseMode::Html) + .await?; + Ok(()) +} + +// Handle the /newlisting command - starts the dialogue (called from within dialogue context) +async fn handle_new_listing(bot: Bot, dialogue: NewListingDialogue, msg: Message) -> HandlerResult { + info!( + "User {} ({}) started new fixed price listing wizard", + msg.chat.username().unwrap_or("unknown"), + msg.chat.id + ); + let response = "🛍️ Creating New Fixed Price Listing\n\n\ Let's create your fixed price listing step by step!\n\n\ Step 1 of 6: Title\n\ @@ -275,54 +369,6 @@ pub async fn handle_description_callback( Ok(()) } -// Create callback query handler for skip button, slots buttons, and start time button -pub fn new_listing_callback_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> { - dptree::entry() - .branch( - dptree::case![ListingWizardState::AwaitingDescription(state)] - .endpoint(handle_description_callback), - ) - .branch( - dptree::case![ListingWizardState::AwaitingSlots(state)].endpoint(handle_slots_callback), - ) - .branch( - dptree::case![ListingWizardState::AwaitingStartTime(state)] - .endpoint(handle_start_time_callback), - ) - .branch( - dptree::case![ListingWizardState::ViewingDraft(state)] - .endpoint(handle_viewing_draft_callback), - ) - .branch( - dptree::case![ListingWizardState::EditingListing(state)] - .endpoint(handle_editing_callback), - ) - .branch( - dptree::case![ListingWizardState::EditingTitle(state)] - .endpoint(handle_edit_field_callback), - ) - .branch( - dptree::case![ListingWizardState::EditingDescription(state)] - .endpoint(handle_edit_field_callback), - ) - .branch( - dptree::case![ListingWizardState::EditingPrice(state)] - .endpoint(handle_edit_field_callback), - ) - .branch( - dptree::case![ListingWizardState::EditingSlots(state)] - .endpoint(handle_edit_field_callback), - ) - .branch( - dptree::case![ListingWizardState::EditingStartTime(state)] - .endpoint(handle_edit_field_callback), - ) - .branch( - dptree::case![ListingWizardState::EditingDuration(state)] - .endpoint(handle_edit_field_callback), - ) -} - pub async fn handle_slots_callback( bot: Bot, dialogue: NewListingDialogue, @@ -609,11 +655,21 @@ async fn process_start_time_and_respond( bot.send_message(chat_id, response) .parse_mode(ParseMode::Html) + .reply_markup(create_duration_keyboard()) .await?; Ok(()) } +fn create_duration_keyboard() -> InlineKeyboardMarkup { + InlineKeyboardMarkup::new([[ + InlineKeyboardButton::callback("1 day", "duration_1_day"), + InlineKeyboardButton::callback("3 days", "duration_3_days"), + InlineKeyboardButton::callback("7 days", "duration_7_days"), + InlineKeyboardButton::callback("14 days", "duration_14_days"), + ]]) +} + pub async fn handle_price_input( bot: Bot, dialogue: NewListingDialogue, @@ -667,16 +723,12 @@ pub async fn handle_price_input( price ); - let slots_buttons = InlineKeyboardMarkup::new([ - [ - InlineKeyboardButton::callback("1", "slots_1"), - InlineKeyboardButton::callback("2", "slots_2"), - ], - [ - InlineKeyboardButton::callback("5", "slots_5"), - InlineKeyboardButton::callback("10", "slots_10"), - ], - ]); + let slots_buttons = InlineKeyboardMarkup::new([[ + InlineKeyboardButton::callback("1", "slots_1"), + InlineKeyboardButton::callback("2", "slots_2"), + InlineKeyboardButton::callback("5", "slots_5"), + InlineKeyboardButton::callback("10", "slots_10"), + ]]); bot.send_message(chat_id, response) .parse_mode(ParseMode::Html) @@ -781,7 +833,7 @@ pub async fn handle_start_time_input( pub async fn handle_duration_input( bot: Bot, dialogue: NewListingDialogue, - mut draft: ListingDraft, + draft: ListingDraft, msg: Message, ) -> HandlerResult { let chat_id = msg.chat.id; @@ -818,14 +870,56 @@ pub async fn handle_duration_input( } }; + process_duration_and_respond(bot, dialogue, draft, chat_id, duration).await?; + Ok(()) +} + +pub async fn handle_duration_callback( + bot: Bot, + dialogue: NewListingDialogue, + draft: ListingDraft, + callback_query: CallbackQuery, +) -> HandlerResult { + let data = match callback_query.data.as_deref() { + Some(data) => data, + None => return Ok(()), + }; + let chat_id = match callback_query.message { + Some(message) => message.chat().id, + _ => return Ok(()), + }; + + let days = match data { + "duration_1_day" => 1, + "duration_3_days" => 3, + "duration_7_days" => 7, + "duration_14_days" => 14, + _ => { + bot.send_message( + chat_id, + "❌ Invalid duration. Please enter number of days (1-14):", + ) + .await?; + return Ok(()); + } + }; + + process_duration_and_respond(bot, dialogue, draft, chat_id, days).await +} + +async fn process_duration_and_respond( + bot: Bot, + dialogue: NewListingDialogue, + mut draft: ListingDraft, + chat_id: ChatId, + duration: i32, +) -> HandlerResult { draft.duration_hours = duration; dialogue .update(ListingWizardState::ViewingDraft(draft.clone())) .await?; - show_confirmation(bot, chat_id, draft).await?; - - Ok(()) + show_confirmation(bot, chat_id, draft).await } async fn show_confirmation(bot: Bot, chat_id: ChatId, state: ListingDraft) -> HandlerResult { diff --git a/src/main.rs b/src/main.rs index 00e3033..5ded2c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,13 +8,13 @@ mod wizard_utils; use anyhow::Result; use log::info; use teloxide::dispatching::dialogue::serializer::Json; -use teloxide::{prelude::*, types::CallbackQuery, utils::command::BotCommands}; +use teloxide::{prelude::*, utils::command::BotCommands}; #[cfg(test)] mod test_utils; -use commands::new_listing::ListingWizardState; use commands::*; use config::Config; +use crate::commands::new_listing::new_listing_handler; use crate::sqlite_storage::SqliteStorage; pub type HandlerResult = anyhow::Result<()>; @@ -52,27 +52,17 @@ async fn main() -> Result<()> { // Create dispatcher with dialogue system Dispatcher::builder( bot, - dptree::entry() - .branch( - Update::filter_message() - .enter_dialogue::, ListingWizardState>() - .branch( - dptree::entry() - .filter_command::() - .branch(dptree::case![Command::Start].endpoint(handle_start)) - .branch(dptree::case![Command::Help].endpoint(handle_help)) - .branch(dptree::case![Command::NewListing].endpoint(handle_new_listing)) - .branch(dptree::case![Command::MyListings].endpoint(handle_my_listings)) - .branch(dptree::case![Command::MyBids].endpoint(handle_my_bids)) - .branch(dptree::case![Command::Settings].endpoint(handle_settings)), - ) - .branch(new_listing_dialogue_handler()), - ) - .branch( - Update::filter_callback_query() - .enter_dialogue::, ListingWizardState>() - .branch(new_listing_callback_handler()), + dptree::entry().branch(new_listing_handler()).branch( + Update::filter_message().branch( + dptree::entry() + .filter_command::() + .branch(dptree::case![Command::Start].endpoint(handle_start)) + .branch(dptree::case![Command::Help].endpoint(handle_help)) + .branch(dptree::case![Command::MyListings].endpoint(handle_my_listings)) + .branch(dptree::case![Command::MyBids].endpoint(handle_my_bids)) + .branch(dptree::case![Command::Settings].endpoint(handle_settings)), ), + ), ) .dependencies(dptree::deps![db_pool, dialog_storage]) .enable_ctrlc_handler()