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
This commit is contained in:
Dylan Knutson
2025-08-28 07:23:40 +00:00
parent 71fe1e60c0
commit 764c17af05
3 changed files with 213 additions and 132 deletions

View File

@@ -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;

View File

@@ -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<ListingWizardState, SqliteStorage<Json>>;
type NewListingDialogue = Dialogue<ListingWizardState, SqliteStorage<Json>>;
// 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::<Message, SqliteStorage<Json>, ListingWizardState>()
.branch(dptree::entry().filter_command::<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::<CallbackQuery, SqliteStorage<Json>, 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 = "🛍️ <b>Creating New Fixed Price Listing</b>\n\n\
Let's create your fixed price listing step by step!\n\n\
<i>Step 1 of 6: Title</i>\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 = "🛍️ <b>Creating New Fixed Price Listing</b>\n\n\
Let's create your fixed price listing step by step!\n\n\
<i>Step 1 of 6: Title</i>\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 {

View File

@@ -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::<Message, SqliteStorage<Json>, ListingWizardState>()
.branch(
dptree::entry()
.filter_command::<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::<CallbackQuery, SqliteStorage<Json>, ListingWizardState>()
.branch(new_listing_callback_handler()),
dptree::entry().branch(new_listing_handler()).branch(
Update::filter_message().branch(
dptree::entry()
.filter_command::<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()