feat: enhance UX with listing type selection and main menu improvements

- Add listing type selection to new listing wizard
  - Create SelectingListingType state for choosing between 4 listing types
  - Add ListingTypeKeyboardButtons with clear descriptions
  - Support BasicAuction, MultiSlotAuction, FixedPriceListing, BlindAuction
  - Update handlers to start with type selection before title input

- Improve main menu navigation
  - Add main menu with action buttons for /start command
  - Create MainMenuButtons with New Listing, My Listings, My Bids, Settings, Help
  - Add back button to My Listings screen for better navigation
  - Implement proper dialogue state management between screens

- Refactor callback handling for type safety
  - Convert string literal matching to enum-based callback handling
  - Use try_from() pattern for all keyboard button callbacks
  - Ensure compile-time safety and exhaustive matching
  - Apply pattern to listing type, slots, duration, and start time callbacks

- Eliminate code duplication
  - Extract reusable main menu functions (enter_main_menu, get_main_menu_message)
  - Centralize main menu logic and message content
  - Update all main menu transitions to use shared functions

- Technical improvements
  - Add proper error handling for invalid callback data
  - Maintain backward compatibility with existing flows
  - Follow established patterns for keyboard button definitions
  - Ensure all changes compile without errors
This commit is contained in:
Dylan Knutson
2025-08-30 05:45:49 +00:00
parent 9ef36b760e
commit 5d7a5b26c1
12 changed files with 347 additions and 86 deletions

View File

@@ -3,9 +3,9 @@ pub mod my_bids;
pub mod my_listings;
pub mod new_listing;
pub mod settings;
pub mod start;
mod start;
pub use help::handle_help;
pub use my_bids::handle_my_bids;
pub use settings::handle_settings;
pub use start::handle_start;
pub use start::{enter_main_menu, handle_main_menu_callback, handle_start};

View File

@@ -1,6 +1,9 @@
use crate::{
case,
commands::new_listing::{enter_edit_listing_draft, ListingDraft},
commands::{
enter_main_menu,
new_listing::{enter_edit_listing_draft, ListingDraft},
},
db::{listing::PersistedListing, user::PersistedUser, ListingDAO, ListingDbId, UserDAO},
keyboard_buttons,
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
@@ -38,6 +41,12 @@ keyboard_buttons! {
}
}
keyboard_buttons! {
enum MyListingsButtons {
BackToMenu("⬅️ Back to Menu", "my_listings_back_to_menu"),
}
}
pub fn my_listings_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
dptree::entry()
.branch(
@@ -74,7 +83,7 @@ async fn handle_my_listings_command_input(
Ok(())
}
async fn show_listings_for_user(
pub async fn show_listings_for_user(
db_pool: SqlitePool,
dialogue: RootDialogue,
bot: Bot,
@@ -101,13 +110,17 @@ async fn show_listings_for_user(
let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?;
if listings.is_empty() {
// Create keyboard with just the back button
let keyboard =
teloxide::types::InlineKeyboardMarkup::new([[MyListingsButtons::BackToMenu.into()]]);
send_message(
&bot,
target,
"📋 <b>My Listings</b>\n\n\
You don't have any listings yet.\n\
Use /newlisting to create your first listing!",
None,
Some(keyboard),
)
.await?;
return Ok(());
@@ -122,6 +135,9 @@ async fn show_listings_for_user(
)]);
}
// Add back to menu button
keyboard = keyboard.append_row(vec![MyListingsButtons::BackToMenu.into()]);
let response = format!(
"📋 <b>My Listings</b>\n\n\
You have {}.\n\n\
@@ -142,6 +158,18 @@ async fn handle_viewing_listings_callback(
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
let target = (from.clone(), message_id);
// Check if it's the back to menu button
if let Ok(button) = MyListingsButtons::try_from(data.as_str()) {
match button {
MyListingsButtons::BackToMenu => {
// Transition back to main menu using the reusable function
enter_main_menu(bot, dialogue, target).await?;
return Ok(());
}
}
}
// Otherwise, treat it as a listing ID
let listing_id = ListingDbId::new(data.parse::<i64>()?);
let (_, listing) =
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
@@ -190,10 +218,7 @@ async fn handle_managing_listing_callback(
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
let target = (from.clone(), message_id);
let button = ManageListingButtons::try_from(data.as_str())
.map_err(|_| anyhow::anyhow!("Invalid ManageListingButtons callback data: {}", data))?;
match button {
match ManageListingButtons::try_from(data.as_str())? {
ManageListingButtons::Edit => {
let (_, listing) =
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;

View File

@@ -4,20 +4,77 @@
//! in the new listing creation and editing workflows.
use crate::{
commands::new_listing::{
field_processing::transition_to_field,
keyboard::*,
messages::{get_keyboard_for_field, get_step_message},
types::{ListingDraft, ListingDraftPersisted, ListingField, NewListingState},
ui::show_confirmation_screen,
commands::{
new_listing::{
field_processing::transition_to_field,
keyboard::{
DurationKeyboardButtons, ListingTypeKeyboardButtons, SlotsKeyboardButtons,
StartTimeKeyboardButtons,
},
messages::{get_keyboard_for_field, get_step_message},
types::{ListingDraft, ListingDraftPersisted, ListingField, NewListingState},
ui::show_confirmation_screen,
},
start::enter_main_menu,
},
db::{listing::ListingFields, ListingDuration},
db::{listing::ListingFields, ListingDuration, ListingType, UserDbId},
message_utils::*,
HandlerResult, RootDialogue,
};
use log::{error, info};
use teloxide::{types::CallbackQuery, Bot};
/// Handle callbacks during the listing type selection phase
pub async fn handle_selecting_listing_type_callback(
bot: Bot,
dialogue: RootDialogue,
seller_id: UserDbId,
callback_query: CallbackQuery,
) -> HandlerResult {
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
info!("User {from:?} selected listing type: {data:?}");
let target = (from, message_id);
// Parse the listing type from callback data
let (listing_type, type_name) = match ListingTypeKeyboardButtons::try_from(data.as_str())? {
ListingTypeKeyboardButtons::FixedPrice => {
(ListingType::FixedPriceListing, "Fixed Price Listing")
}
ListingTypeKeyboardButtons::BasicAuction => (ListingType::BasicAuction, "Basic Auction"),
ListingTypeKeyboardButtons::BlindAuction => (ListingType::BlindAuction, "Blind Auction"),
ListingTypeKeyboardButtons::MultiSlot => {
(ListingType::MultiSlotAuction, "Multi-Slot Auction")
}
ListingTypeKeyboardButtons::Back => {
enter_main_menu(bot, dialogue, target).await?;
return Ok(());
}
};
// Create draft with selected listing type
let draft = ListingDraft::new_for_seller_with_type(seller_id, listing_type);
// Transition to first field (Title)
transition_to_field(dialogue, ListingField::Title, draft).await?;
let response = format!(
"✅ <b>{} selected!</b>\n\n\
Let's create your listing step by step!\n\n{}",
type_name,
get_step_message(ListingField::Title)
);
send_message(
&bot,
target,
response,
get_keyboard_for_field(ListingField::Title),
)
.await?;
Ok(())
}
/// Handle callbacks during the field input phase
pub async fn handle_awaiting_draft_field_callback(
bot: Bot,
@@ -38,14 +95,17 @@ pub async fn handle_awaiting_draft_field_callback(
ListingField::Description if data == "skip" => {
handle_description_skip_callback(&bot, dialogue, draft, target).await
}
ListingField::Slots if data.starts_with("slots_") => {
handle_slots_callback(&bot, dialogue, draft, &data, target).await
ListingField::Slots => {
let button = SlotsKeyboardButtons::try_from(data.as_str())?;
handle_slots_callback(&bot, dialogue, draft, button, target).await
}
ListingField::StartTime if data.starts_with("start_time_") => {
handle_start_time_callback(&bot, dialogue, draft, &data, target).await
ListingField::StartTime => {
let button = StartTimeKeyboardButtons::try_from(data.as_str())?;
handle_start_time_callback(&bot, dialogue, draft, button, target).await
}
ListingField::Duration if data.starts_with("duration_") => {
handle_duration_callback(&bot, dialogue, draft, &data, target).await
ListingField::Duration => {
let button = DurationKeyboardButtons::try_from(data.as_str())?;
handle_duration_callback(&bot, dialogue, draft, button, target).await
}
_ => {
error!("Unknown callback data for field {field:?}: {data}");
@@ -84,12 +144,10 @@ async fn handle_slots_callback(
bot: &Bot,
dialogue: RootDialogue,
mut draft: ListingDraft,
data: &str,
button: SlotsKeyboardButtons,
target: impl Into<MessageTarget>,
) -> HandlerResult {
let target = target.into();
let button = SlotsKeyboardButtons::try_from(data)
.map_err(|_| anyhow::anyhow!("Unknown SlotsKeyboardButtons data: {}", data))?;
let num_slots = match button {
SlotsKeyboardButtons::OneSlot => 1,
SlotsKeyboardButtons::TwoSlots => 2,
@@ -124,12 +182,10 @@ async fn handle_start_time_callback(
bot: &Bot,
dialogue: RootDialogue,
mut draft: ListingDraft,
data: &str,
button: StartTimeKeyboardButtons,
target: impl Into<MessageTarget>,
) -> HandlerResult {
let target = target.into();
let button = StartTimeKeyboardButtons::try_from(data)
.map_err(|_| anyhow::anyhow!("Unknown StartTimeKeyboardButtons data: {}", data))?;
let start_time = match button {
StartTimeKeyboardButtons::Now => ListingDuration::zero(),
};
@@ -163,11 +219,10 @@ async fn handle_duration_callback(
bot: &Bot,
dialogue: RootDialogue,
mut draft: ListingDraft,
data: &str,
button: DurationKeyboardButtons,
target: impl Into<MessageTarget>,
) -> HandlerResult {
let target = target.into();
let button = DurationKeyboardButtons::try_from(data).unwrap();
let duration = ListingDuration::days(match button {
DurationKeyboardButtons::OneDay => 1,
DurationKeyboardButtons::ThreeDays => 3,

View File

@@ -28,6 +28,12 @@ pub fn new_listing_handler() -> Handler {
)
.branch(
Update::filter_callback_query()
.branch(
case![DialogueRootState::NewListing(
NewListingState::SelectingListingType { seller_id }
)]
.endpoint(handle_selecting_listing_type_callback),
)
.branch(
case![DialogueRootState::NewListing(
NewListingState::AwaitingDraftField { field, draft }

View File

@@ -12,7 +12,8 @@ use crate::{
SlotsKeyboardButtons, StartTimeKeyboardButtons,
},
messages::{
get_edit_success_message, get_keyboard_for_field, get_step_message, get_success_message,
get_edit_success_message, get_keyboard_for_field, get_listing_type_keyboard,
get_listing_type_selection_message, get_step_message, get_success_message,
},
types::{ListingDraft, ListingDraftPersisted, ListingField, NewListingState},
ui::{display_listing_summary, show_confirmation_screen},
@@ -29,38 +30,38 @@ use sqlx::SqlitePool;
use teloxide::{prelude::*, types::*, Bot};
/// Handle the /newlisting command - starts the dialogue
pub async fn handle_new_listing_command(
pub(super) async fn handle_new_listing_command(
db_pool: SqlitePool,
bot: Bot,
dialogue: RootDialogue,
msg: Message,
) -> HandlerResult {
info!(
"User {} started new fixed price listing wizard",
HandleAndId::from_chat(&msg.chat),
);
let user = msg.from.ok_or_else(|| anyhow::anyhow!("User not found"))?;
enter_handle_new_listing(db_pool, bot, dialogue, user, msg.chat).await?;
Ok(())
}
pub async fn enter_handle_new_listing(
db_pool: SqlitePool,
bot: Bot,
dialogue: RootDialogue,
user: User,
target: impl Into<MessageTarget>,
) -> HandlerResult {
let user = UserDAO::find_or_create_by_telegram_user(&db_pool, user).await?;
// Initialize the dialogue to Start state
// Initialize the dialogue to listing type selection state
dialogue
.update(NewListingState::AwaitingDraftField {
field: ListingField::Title,
draft: ListingDraft::new_for_seller(user.persisted.id),
.update(NewListingState::SelectingListingType {
seller_id: user.persisted.id,
})
.await?;
let response = format!(
"🛍️ <b>Creating New Fixed Price Listing</b>\n\n\
Let's create your fixed price listing step by step!\n\n{}",
get_step_message(ListingField::Title)
);
send_message(
&bot,
msg.chat,
response,
get_keyboard_for_field(ListingField::Title),
target,
get_listing_type_selection_message(),
Some(get_listing_type_keyboard()),
)
.await?;
Ok(())
@@ -154,10 +155,7 @@ pub async fn handle_viewing_draft_callback(
let target = (from.clone(), message_id);
let button = ConfirmationKeyboardButtons::try_from(data.as_str())
.map_err(|_| anyhow::anyhow!("Unknown ConfirmationKeyboardButtons data: {}", data))?;
match button {
match ConfirmationKeyboardButtons::try_from(data.as_str())? {
ConfirmationKeyboardButtons::Create | ConfirmationKeyboardButtons::Save => {
info!("User {target:?} confirmed listing creation");
save_listing(db_pool, bot, target, draft).await?;
@@ -197,11 +195,9 @@ pub async fn handle_editing_draft_callback(
) -> HandlerResult {
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
let target = (from, message_id);
let button = FieldSelectionKeyboardButtons::try_from(data.as_str())
.map_err(|e| anyhow::anyhow!("Invalid field selection button: {}", e))?;
info!("User {target:?} in editing screen, showing field selection");
let button = FieldSelectionKeyboardButtons::try_from(data.as_str())?;
if button == FieldSelectionKeyboardButtons::Done {
show_confirmation_screen(&bot, target, &draft).await?;
dialogue
@@ -219,7 +215,9 @@ pub async fn handle_editing_draft_callback(
FieldSelectionKeyboardButtons::Slots => ListingField::Slots,
FieldSelectionKeyboardButtons::StartTime => ListingField::StartTime,
FieldSelectionKeyboardButtons::Duration => ListingField::Duration,
FieldSelectionKeyboardButtons::Done => unreachable!(),
FieldSelectionKeyboardButtons::Done => {
return Err(anyhow::anyhow!("Done button should not be used here"))
}
};
let value = get_current_field_value(&draft, field)?;

View File

@@ -53,3 +53,17 @@ keyboard_buttons! {
Now("Now", "start_time_now"),
}
}
keyboard_buttons! {
pub enum ListingTypeKeyboardButtons {
[
FixedPrice("🛍️ Fixed Price", "listing_type_fixed_price"),
BasicAuction("⏰ Basic Auction", "listing_type_basic_auction"),
],
[
BlindAuction("🎭 Blind Auction", "listing_type_blind_auction"),
MultiSlot("🎯 Multi-Slot Auction", "listing_type_multi_slot"),
],
[Back("🔙 Back", "listing_type_back"),]
}
}

View File

@@ -71,3 +71,18 @@ fn create_cancel_keyboard() -> InlineKeyboardMarkup {
fn create_skip_cancel_keyboard() -> InlineKeyboardMarkup {
create_multi_row_keyboard(&[&[("Skip", "skip"), ("Cancel", "cancel")]])
}
/// Get the listing type selection message
pub fn get_listing_type_selection_message() -> &'static str {
"🛍️ <b>What type of listing would you like to create?</b>\n\n\
<b>🛍️ Fixed Price:</b> Set a fixed price for immediate purchase\n\
<b>⏰ Basic Auction:</b> Traditional time-based auction with bidding\n\
<b>🎭 Blind Auction:</b> Buyers submit sealed bids, you choose the winner\n\
<b>🎯 Multi-Slot Auction:</b> Multiple items/winners in one auction\n\n\
Choose your listing type:"
}
/// Get the keyboard for listing type selection
pub fn get_listing_type_keyboard() -> InlineKeyboardMarkup {
ListingTypeKeyboardButtons::to_keyboard()
}

View File

@@ -17,7 +17,7 @@ mod field_processing;
mod handler_factory;
mod handlers;
mod keyboard;
mod messages;
pub mod messages;
#[cfg(test)]
mod tests;
@@ -27,5 +27,5 @@ mod validations;
// Re-export the main handler for external use
pub use handler_factory::new_listing_handler;
pub use handlers::enter_edit_listing_draft;
pub use handlers::{enter_edit_listing_draft, enter_handle_new_listing};
pub use types::*;

View File

@@ -1,10 +1,11 @@
use crate::{
db::{
listing::{
FixedPriceListingFields, ListingBase, ListingFields, NewListingFields,
PersistedListing, PersistedListingFields,
BasicAuctionFields, BlindAuctionFields, FixedPriceListingFields, ListingBase,
ListingFields, MultiSlotAuctionFields, NewListingFields, PersistedListing,
PersistedListingFields,
},
MoneyAmount, UserDbId,
ListingType, MoneyAmount, UserDbId,
},
DialogueRootState,
};
@@ -19,7 +20,34 @@ pub struct ListingDraft {
}
impl ListingDraft {
pub fn new_for_seller(seller_id: UserDbId) -> Self {
pub fn new_for_seller_with_type(seller_id: UserDbId, listing_type: ListingType) -> Self {
let fields = match listing_type {
ListingType::BasicAuction => ListingFields::BasicAuction(BasicAuctionFields {
starting_bid: MoneyAmount::default(),
buy_now_price: None,
min_increment: MoneyAmount::from_cents(100), // Default $1.00 increment
anti_snipe_minutes: Some(5),
}),
ListingType::MultiSlotAuction => {
ListingFields::MultiSlotAuction(MultiSlotAuctionFields {
starting_bid: MoneyAmount::default(),
buy_now_price: MoneyAmount::default(),
min_increment: Some(MoneyAmount::from_cents(100)), // Default $1.00 increment
slots_available: 1,
anti_snipe_minutes: 5,
})
}
ListingType::FixedPriceListing => {
ListingFields::FixedPriceListing(FixedPriceListingFields {
buy_now_price: MoneyAmount::default(),
slots_available: 1,
})
}
ListingType::BlindAuction => ListingFields::BlindAuction(BlindAuctionFields {
starting_bid: MoneyAmount::default(),
}),
};
Self {
has_changes: false,
persisted: ListingDraftPersisted::New(NewListingFields::default()),
@@ -28,10 +56,7 @@ impl ListingDraft {
title: "".to_string(),
description: None,
},
fields: ListingFields::FixedPriceListing(FixedPriceListingFields {
buy_now_price: MoneyAmount::default(),
slots_available: 0,
}),
fields,
}
}
@@ -64,6 +89,9 @@ pub enum ListingField {
// Dialogue state for the new listing wizard
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum NewListingState {
SelectingListingType {
seller_id: UserDbId,
},
AwaitingDraftField {
field: ListingField,
draft: ListingDraft,

View File

@@ -1,24 +1,140 @@
use log::info;
use teloxide::{prelude::*, types::Message, Bot};
use teloxide::{
types::{CallbackQuery, Message},
utils::command::BotCommands,
Bot,
};
use crate::HandlerResult;
use sqlx::SqlitePool;
pub async fn handle_start(bot: Bot, msg: Message) -> HandlerResult {
let welcome_message = "🎯 Welcome to Pawctioneer Bot! 🎯\n\n\
This bot helps you participate in various types of auctions:\n\
• Standard auctions with anti-sniping protection\n\
• Multi-slot auctions (multiple winners)\n\
• Fixed price sales\n\
• Blind auctions\n\n\
Use /help to see all available commands.\n\n\
Ready to start your auction experience? 🚀";
use crate::{
commands::{my_listings::show_listings_for_user, new_listing::enter_handle_new_listing},
keyboard_buttons,
message_utils::{extract_callback_data, send_message, MessageTarget},
Command, DialogueRootState, HandlerResult, RootDialogue,
};
info!(
"User {} ({}) started the bot",
msg.chat.username().unwrap_or("unknown"),
msg.chat.id
);
keyboard_buttons! {
pub enum MainMenuButtons {
[
NewListing("🛍️ New Listing", "menu_new_listing"),
],
[
MyListings("📋 My Listings", "menu_my_listings"),
MyBids("💰 My Bids", "menu_my_bids"),
],
[
Settings("⚙️ Settings", "menu_settings"),
Help("❓ Help", "menu_help"),
]
}
}
/// Get the main menu welcome message
pub fn get_main_menu_message() -> &'static str {
"🎯 <b>Welcome to Pawctioneer Bot!</b> 🎯\n\n\
This bot helps you participate in various types of auctions:\n\
• Standard auctions with anti-sniping protection\n\
• Multi-slot auctions (multiple winners)\n\
• Fixed price sales\n\
• Blind auctions\n\n\
Choose an option below to get started! 🚀"
}
pub async fn handle_start(bot: Bot, dialogue: RootDialogue, msg: Message) -> HandlerResult {
enter_main_menu(bot, dialogue, msg.chat).await?;
Ok(())
}
/// Show the main menu with buttons
pub async fn enter_main_menu(
bot: Bot,
dialogue: RootDialogue,
target: impl Into<MessageTarget>,
) -> HandlerResult {
dialogue.update(DialogueRootState::MainMenu).await?;
send_message(
&bot,
target,
get_main_menu_message(),
Some(MainMenuButtons::to_keyboard()),
)
.await?;
Ok(())
}
pub async fn handle_main_menu_callback(
db_pool: SqlitePool,
bot: Bot,
dialogue: RootDialogue,
callback_query: CallbackQuery,
) -> HandlerResult {
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
let target = MessageTarget::from((from.clone(), message_id));
info!(
"User {} selected main menu option: {}",
from.username.as_deref().unwrap_or("unknown"),
data
);
let button = MainMenuButtons::try_from(data.as_str())?;
match button {
MainMenuButtons::NewListing => {
enter_handle_new_listing(db_pool, bot, dialogue, from.clone(), target).await?;
}
MainMenuButtons::MyListings => {
// Call show_listings_for_user directly
show_listings_for_user(db_pool, dialogue, bot, from.id, target).await?;
}
MainMenuButtons::MyBids => {
send_message(
&bot,
target,
"💰 <b>My Bids (Coming Soon)</b>\n\n\
Here you'll be able to view:\n\
• Your active bids\n\
• Bid history\n\
• Won/lost auctions\n\
• Outbid notifications\n\n\
Feature in development! 🛠️",
Some(MainMenuButtons::to_keyboard()),
)
.await?;
}
MainMenuButtons::Settings => {
send_message(
&bot,
target,
"⚙️ <b>Settings (Coming Soon)</b>\n\n\
Here you'll be able to configure:\n\
• Notification preferences\n\
• Language settings\n\
• Default bid increments\n\
• Outbid alerts\n\n\
Feature in development! 🛠️",
Some(MainMenuButtons::to_keyboard()),
)
.await?;
}
MainMenuButtons::Help => {
let help_message = format!(
"📋 <b>Available Commands:</b>\n\n{}\n\n\
📧 <b>Support:</b> Contact @admin for help\n\
🔗 <b>More info:</b> Use individual commands to get started!",
Command::descriptions()
);
send_message(
&bot,
target,
help_message,
Some(MainMenuButtons::to_keyboard()),
)
.await?;
}
}
bot.send_message(msg.chat.id, welcome_message).await?;
Ok(())
}

View File

@@ -50,13 +50,13 @@ macro_rules! keyboard_buttons {
}
}
impl<'a> TryFrom<&'a str> for $name {
type Error = &'a str;
type Error = anyhow::Error;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
match value {
$($(
$callback_data => Ok(Self::$variant),
)*)*
_ => Err(value),
_ => anyhow::bail!("Unknown {name} button: {value}", name = stringify!($name)),
}
}
}

View File

@@ -62,6 +62,7 @@ pub enum Command {
enum DialogueRootState {
#[default]
Start,
MainMenu,
NewListing(NewListingState),
MyListings(MyListingsState),
}
@@ -91,6 +92,9 @@ async fn main() -> Result<()> {
.enter_dialogue::<Update, SqliteStorage<Json>, DialogueRootState>()
.branch(new_listing_handler())
.branch(my_listings_handler())
.branch(Update::filter_callback_query().branch(
dptree::case![DialogueRootState::MainMenu].endpoint(handle_main_menu_callback),
))
.branch(
Update::filter_message().branch(
dptree::entry()