Compare commits
3 Commits
9ef36b760e
...
65a50b05e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65a50b05e2 | ||
|
|
c9a824dd88 | ||
|
|
5d7a5b26c1 |
@@ -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};
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
use crate::{
|
||||
case,
|
||||
commands::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},
|
||||
Command, DialogueRootState, HandlerResult, RootDialogue,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
use teloxide::{
|
||||
dispatching::{DpHandlerDescription, UpdateFilterExt},
|
||||
prelude::*,
|
||||
types::{InlineKeyboardButton, Message},
|
||||
Bot,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum MyListingsState {
|
||||
ViewingListings,
|
||||
ManagingListing(ListingDbId),
|
||||
}
|
||||
impl From<MyListingsState> for DialogueRootState {
|
||||
fn from(state: MyListingsState) -> Self {
|
||||
DialogueRootState::MyListings(state)
|
||||
}
|
||||
}
|
||||
|
||||
keyboard_buttons! {
|
||||
enum ManageListingButtons {
|
||||
[
|
||||
Edit("✏️ Edit", "manage_listing_edit"),
|
||||
Delete("🗑️ Delete", "manage_listing_delete"),
|
||||
],
|
||||
[
|
||||
Back("⬅️ Back", "manage_listing_back"),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn my_listings_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
||||
dptree::entry()
|
||||
.branch(
|
||||
Update::filter_message().filter_command::<Command>().branch(
|
||||
dptree::case![Command::MyListings].endpoint(handle_my_listings_command_input),
|
||||
),
|
||||
)
|
||||
.branch(
|
||||
Update::filter_callback_query()
|
||||
.branch(
|
||||
// Callback when user taps a listing ID button to manage that listing
|
||||
case![DialogueRootState::MyListings(
|
||||
MyListingsState::ViewingListings
|
||||
)]
|
||||
.endpoint(handle_viewing_listings_callback),
|
||||
)
|
||||
.branch(
|
||||
case![DialogueRootState::MyListings(
|
||||
MyListingsState::ManagingListing(listing_id)
|
||||
)]
|
||||
.endpoint(handle_managing_listing_callback),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_my_listings_command_input(
|
||||
db_pool: SqlitePool,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
msg: Message,
|
||||
) -> HandlerResult {
|
||||
let from = msg.from.unwrap();
|
||||
show_listings_for_user(db_pool, dialogue, bot, from.id, msg.chat).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_listings_for_user(
|
||||
db_pool: SqlitePool,
|
||||
dialogue: RootDialogue,
|
||||
bot: Bot,
|
||||
user: teloxide::types::UserId,
|
||||
target: impl Into<MessageTarget>,
|
||||
) -> HandlerResult {
|
||||
// If we reach here, show the listings menu
|
||||
let user = match UserDAO::find_by_telegram_id(&db_pool, user).await? {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
send_message(
|
||||
&bot,
|
||||
target,
|
||||
"You don't have an account. Try creating an auction first.",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
return Err(anyhow::anyhow!("User not found"));
|
||||
}
|
||||
};
|
||||
|
||||
// Transition to ViewingListings state
|
||||
dialogue.update(MyListingsState::ViewingListings).await?;
|
||||
|
||||
let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?;
|
||||
if listings.is_empty() {
|
||||
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,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create keyboard with buttons for each listing
|
||||
let mut keyboard = teloxide::types::InlineKeyboardMarkup::default();
|
||||
for listing in &listings {
|
||||
keyboard = keyboard.append_row(vec![InlineKeyboardButton::callback(
|
||||
listing.base.title.to_string(),
|
||||
listing.persisted.id.to_string(),
|
||||
)]);
|
||||
}
|
||||
|
||||
let response = format!(
|
||||
"📋 <b>My Listings</b>\n\n\
|
||||
You have {}.\n\n\
|
||||
Select a listing to view details",
|
||||
pluralize_with_count(listings.len(), "listing", "listings")
|
||||
);
|
||||
|
||||
send_message(&bot, target, response, Some(keyboard)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_viewing_listings_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 = (from.clone(), message_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?;
|
||||
dialogue
|
||||
.update(MyListingsState::ManagingListing(listing_id))
|
||||
.await?;
|
||||
show_listing_details(&bot, listing, target).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_listing_details(
|
||||
bot: &Bot,
|
||||
listing: PersistedListing,
|
||||
target: impl Into<MessageTarget>,
|
||||
) -> HandlerResult {
|
||||
let response = format!(
|
||||
"🔍 <b>Viewing Listing Details</b>\n\n\
|
||||
<b>Title:</b> {}\n\
|
||||
<b>Description:</b> {}\n",
|
||||
listing.base.title,
|
||||
listing
|
||||
.base
|
||||
.description
|
||||
.as_deref()
|
||||
.unwrap_or("No description"),
|
||||
);
|
||||
|
||||
send_message(
|
||||
bot,
|
||||
target,
|
||||
response,
|
||||
Some(ManageListingButtons::to_keyboard()),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_managing_listing_callback(
|
||||
db_pool: SqlitePool,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
callback_query: CallbackQuery,
|
||||
listing_id: ListingDbId,
|
||||
) -> HandlerResult {
|
||||
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 {
|
||||
ManageListingButtons::Edit => {
|
||||
let (_, listing) =
|
||||
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
||||
let draft = ListingDraft::from_persisted(listing);
|
||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
||||
}
|
||||
ManageListingButtons::Delete => {
|
||||
ListingDAO::delete_listing(&db_pool, listing_id).await?;
|
||||
send_message(&bot, target, "Listing deleted.", None).await?;
|
||||
}
|
||||
ManageListingButtons::Back => {
|
||||
dialogue.update(MyListingsState::ViewingListings).await?;
|
||||
show_listings_for_user(db_pool, dialogue, bot, from.id, target).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_and_listing(
|
||||
db_pool: &SqlitePool,
|
||||
bot: &Bot,
|
||||
user_id: teloxide::types::UserId,
|
||||
listing_id: ListingDbId,
|
||||
target: impl Into<MessageTarget>,
|
||||
) -> HandlerResult<(PersistedUser, PersistedListing)> {
|
||||
let user = match UserDAO::find_by_telegram_id(db_pool, user_id).await? {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
send_message(
|
||||
bot,
|
||||
target,
|
||||
"❌ You don't have an account. Try creating an auction first.",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
return Err(anyhow::anyhow!("User not found"));
|
||||
}
|
||||
};
|
||||
|
||||
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"));
|
||||
}
|
||||
};
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
Ok((user, listing))
|
||||
}
|
||||
19
src/commands/my_listings/keyboard.rs
Normal file
19
src/commands/my_listings/keyboard.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::keyboard_buttons;
|
||||
|
||||
keyboard_buttons! {
|
||||
pub enum MyListingsButtons {
|
||||
BackToMenu("⬅️ Back to Menu", "my_listings_back_to_menu"),
|
||||
}
|
||||
}
|
||||
|
||||
keyboard_buttons! {
|
||||
pub enum ManageListingButtons {
|
||||
[
|
||||
PreviewMessage("👀 Preview", "manage_listing_preview"),
|
||||
ForwardListing("↪️ Forward", "manage_listing_forward"),
|
||||
],
|
||||
[Edit("✏️ Edit", "manage_listing_edit"),],
|
||||
[Delete("🗑️ Delete", "manage_listing_delete"),],
|
||||
[Back("⬅️ Back", "manage_listing_back"),]
|
||||
}
|
||||
}
|
||||
467
src/commands/my_listings/mod.rs
Normal file
467
src/commands/my_listings/mod.rs
Normal file
@@ -0,0 +1,467 @@
|
||||
mod keyboard;
|
||||
|
||||
use crate::{
|
||||
case,
|
||||
commands::{
|
||||
enter_main_menu,
|
||||
my_listings::keyboard::{ManageListingButtons, MyListingsButtons},
|
||||
new_listing::{enter_edit_listing_draft, ListingDraft},
|
||||
},
|
||||
db::{
|
||||
listing::{ListingFields, PersistedListing},
|
||||
user::PersistedUser,
|
||||
ListingDAO, ListingDbId, UserDAO,
|
||||
},
|
||||
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
|
||||
Command, DialogueRootState, HandlerResult, RootDialogue,
|
||||
};
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
use teloxide::{
|
||||
dispatching::{DpHandlerDescription, UpdateFilterExt},
|
||||
prelude::*,
|
||||
types::{
|
||||
InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResult, InlineQueryResultArticle,
|
||||
InputMessageContent, InputMessageContentText, Message, ParseMode, User,
|
||||
},
|
||||
Bot,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum MyListingsState {
|
||||
ViewingListings,
|
||||
ManagingListing(ListingDbId),
|
||||
}
|
||||
impl From<MyListingsState> for DialogueRootState {
|
||||
fn from(state: MyListingsState) -> Self {
|
||||
DialogueRootState::MyListings(state)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn my_listings_inline_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
||||
Update::filter_inline_query()
|
||||
.inspect(|query: InlineQuery| info!("Received inline query: {:?}", query))
|
||||
.filter_map_async(inline_query_extract_forward_listing)
|
||||
.endpoint(handle_forward_listing)
|
||||
}
|
||||
|
||||
pub fn my_listings_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
||||
dptree::entry()
|
||||
.branch(
|
||||
Update::filter_message().filter_command::<Command>().branch(
|
||||
dptree::case![Command::MyListings].endpoint(handle_my_listings_command_input),
|
||||
),
|
||||
)
|
||||
.branch(
|
||||
Update::filter_callback_query()
|
||||
.branch(
|
||||
// Callback when user taps a listing ID button to manage that listing
|
||||
case![DialogueRootState::MyListings(
|
||||
MyListingsState::ViewingListings
|
||||
)]
|
||||
.endpoint(handle_viewing_listings_callback),
|
||||
)
|
||||
.branch(
|
||||
case![DialogueRootState::MyListings(
|
||||
MyListingsState::ManagingListing(listing_id)
|
||||
)]
|
||||
.endpoint(handle_managing_listing_callback),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async fn inline_query_extract_forward_listing(
|
||||
db_pool: SqlitePool,
|
||||
inline_query: InlineQuery,
|
||||
) -> Option<PersistedListing> {
|
||||
let query = &inline_query.query;
|
||||
info!("Try to extract forward listing from query: {}", query);
|
||||
let listing_id_str = query.split("forward_listing:").nth(1)?;
|
||||
let listing_id = ListingDbId::new(listing_id_str.parse::<i64>().ok()?);
|
||||
let listing = ListingDAO::find_by_id(&db_pool, listing_id)
|
||||
.await
|
||||
.unwrap_or(None)?;
|
||||
Some(listing)
|
||||
}
|
||||
|
||||
async fn handle_forward_listing(
|
||||
bot: Bot,
|
||||
inline_query: InlineQuery,
|
||||
listing: PersistedListing,
|
||||
) -> HandlerResult {
|
||||
info!(
|
||||
"Handling forward listing inline query for listing {:?}",
|
||||
listing
|
||||
);
|
||||
|
||||
let bot_username = match bot.get_me().await?.username.as_ref() {
|
||||
Some(username) => username.to_string(),
|
||||
None => anyhow::bail!("Bot username not found"),
|
||||
};
|
||||
|
||||
// Create inline keyboard with auction interaction buttons
|
||||
let keyboard = InlineKeyboardMarkup::default()
|
||||
.append_row([
|
||||
InlineKeyboardButton::callback(
|
||||
"💰 Place Bid",
|
||||
format!("inline_bid:{}", listing.persisted.id),
|
||||
),
|
||||
InlineKeyboardButton::callback(
|
||||
"👀 Watch",
|
||||
format!("inline_watch:{}", listing.persisted.id),
|
||||
),
|
||||
])
|
||||
.append_row([InlineKeyboardButton::url(
|
||||
"🔗 View Full Details",
|
||||
format!(
|
||||
"https://t.me/{}?start=listing:{}",
|
||||
bot_username, listing.persisted.id
|
||||
)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)]);
|
||||
|
||||
// Get the current price based on listing type
|
||||
let current_price = get_listing_current_price(&listing);
|
||||
|
||||
// Create a more detailed message content for the shared listing
|
||||
let message_content = format!(
|
||||
"🎯 <b>{}</b>\n\n\
|
||||
📝 {}\n\n\
|
||||
💰 <b>Current Price:</b> ${}\n\
|
||||
⏰ <b>Ends:</b> {}\n\n\
|
||||
<i>Use the buttons below to interact! ⬇️</i>",
|
||||
listing.base.title,
|
||||
listing
|
||||
.base
|
||||
.description
|
||||
.as_deref()
|
||||
.unwrap_or("No description"),
|
||||
current_price,
|
||||
listing.persisted.end_at.format("%b %d, %Y at %H:%M UTC")
|
||||
);
|
||||
|
||||
bot.answer_inline_query(
|
||||
inline_query.id,
|
||||
[InlineQueryResult::Article(
|
||||
InlineQueryResultArticle::new(
|
||||
listing.persisted.id.to_string(),
|
||||
format!("💰 {} - ${}", listing.base.title, current_price),
|
||||
InputMessageContent::Text(
|
||||
InputMessageContentText::new(message_content).parse_mode(ParseMode::Html),
|
||||
),
|
||||
)
|
||||
.description(&listing.base.title)
|
||||
.reply_markup(keyboard), // Add the inline keyboard here!
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to get the current price of a listing based on its type
|
||||
fn get_listing_current_price(listing: &PersistedListing) -> String {
|
||||
use crate::db::listing::ListingFields;
|
||||
|
||||
match &listing.fields {
|
||||
ListingFields::BasicAuction(fields) => {
|
||||
// For basic auctions, show starting bid (in a real app, you'd show current highest bid)
|
||||
format!("{}", fields.starting_bid)
|
||||
}
|
||||
ListingFields::MultiSlotAuction(fields) => {
|
||||
// For multi-slot auctions, show starting bid
|
||||
format!("{}", fields.starting_bid)
|
||||
}
|
||||
ListingFields::FixedPriceListing(fields) => {
|
||||
// For fixed price listings, show the fixed price
|
||||
format!("{}", fields.buy_now_price)
|
||||
}
|
||||
ListingFields::BlindAuction(fields) => {
|
||||
// For blind auctions, show starting bid
|
||||
format!("{}", fields.starting_bid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_my_listings_command_input(
|
||||
db_pool: SqlitePool,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
msg: Message,
|
||||
) -> HandlerResult {
|
||||
let from = msg.from.unwrap();
|
||||
show_listings_for_user(db_pool, dialogue, bot, from.id, msg.chat).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn show_listings_for_user(
|
||||
db_pool: SqlitePool,
|
||||
dialogue: RootDialogue,
|
||||
bot: Bot,
|
||||
user: teloxide::types::UserId,
|
||||
target: impl Into<MessageTarget>,
|
||||
) -> HandlerResult {
|
||||
// If we reach here, show the listings menu
|
||||
let user = match UserDAO::find_by_telegram_id(&db_pool, user).await? {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
send_message(
|
||||
&bot,
|
||||
target,
|
||||
"You don't have an account. Try creating an auction first.",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
return Err(anyhow::anyhow!("User not found"));
|
||||
}
|
||||
};
|
||||
|
||||
// Transition to ViewingListings state
|
||||
dialogue.update(MyListingsState::ViewingListings).await?;
|
||||
|
||||
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!",
|
||||
Some(keyboard),
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create keyboard with buttons for each listing
|
||||
let mut keyboard = teloxide::types::InlineKeyboardMarkup::default();
|
||||
for listing in &listings {
|
||||
keyboard = keyboard.append_row(vec![InlineKeyboardButton::callback(
|
||||
listing.base.title.to_string(),
|
||||
listing.persisted.id.to_string(),
|
||||
)]);
|
||||
}
|
||||
|
||||
// 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\
|
||||
Select a listing to view details",
|
||||
pluralize_with_count(listings.len(), "listing", "listings")
|
||||
);
|
||||
|
||||
send_message(&bot, target, response, Some(keyboard)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_viewing_listings_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 = (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?;
|
||||
dialogue
|
||||
.update(MyListingsState::ManagingListing(listing_id))
|
||||
.await?;
|
||||
show_listing_details(&bot, listing, target).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_listing_details(
|
||||
bot: &Bot,
|
||||
listing: PersistedListing,
|
||||
target: impl Into<MessageTarget>,
|
||||
) -> HandlerResult {
|
||||
let response = format!(
|
||||
"🔍 <b>Listing Details</b>\n\n\
|
||||
<b>Title:</b> {}\n\
|
||||
<b>Description:</b> {}\n",
|
||||
listing.base.title,
|
||||
listing
|
||||
.base
|
||||
.description
|
||||
.as_deref()
|
||||
.unwrap_or("No description"),
|
||||
);
|
||||
|
||||
send_message(
|
||||
bot,
|
||||
target,
|
||||
response,
|
||||
Some(
|
||||
InlineKeyboardMarkup::default()
|
||||
.append_row([
|
||||
ManageListingButtons::PreviewMessage.to_button(),
|
||||
InlineKeyboardButton::switch_inline_query(
|
||||
ManageListingButtons::ForwardListing.title(),
|
||||
format!("forward_listing:{}", listing.persisted.id),
|
||||
),
|
||||
])
|
||||
.append_row([
|
||||
ManageListingButtons::Edit.to_button(),
|
||||
ManageListingButtons::Delete.to_button(),
|
||||
])
|
||||
.append_row([ManageListingButtons::Back.to_button()]),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_managing_listing_callback(
|
||||
db_pool: SqlitePool,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
callback_query: CallbackQuery,
|
||||
listing_id: ListingDbId,
|
||||
) -> HandlerResult {
|
||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||
let target = (from.clone(), message_id);
|
||||
|
||||
match ManageListingButtons::try_from(data.as_str())? {
|
||||
ManageListingButtons::PreviewMessage => {
|
||||
let (_, listing) =
|
||||
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
||||
send_preview_listing_message(&bot, listing, from).await?;
|
||||
}
|
||||
ManageListingButtons::ForwardListing => {
|
||||
unimplemented!("Forward listing not implemented");
|
||||
}
|
||||
ManageListingButtons::Edit => {
|
||||
let (_, listing) =
|
||||
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
||||
let draft = ListingDraft::from_persisted(listing);
|
||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
||||
}
|
||||
ManageListingButtons::Delete => {
|
||||
ListingDAO::delete_listing(&db_pool, listing_id).await?;
|
||||
send_message(&bot, target, "Listing deleted.", None).await?;
|
||||
}
|
||||
ManageListingButtons::Back => {
|
||||
dialogue.update(MyListingsState::ViewingListings).await?;
|
||||
show_listings_for_user(db_pool, dialogue, bot, from.id, target).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keyboard_for_listing(listing: &PersistedListing) -> InlineKeyboardMarkup {
|
||||
let mut keyboard = InlineKeyboardMarkup::default();
|
||||
match listing.fields {
|
||||
ListingFields::FixedPriceListing(_) => {
|
||||
keyboard = keyboard.append_row([InlineKeyboardButton::callback(
|
||||
"Buy",
|
||||
"fixed_price_listing_buy",
|
||||
)]);
|
||||
}
|
||||
ListingFields::BasicAuction(_) => {
|
||||
keyboard = keyboard.append_row([
|
||||
InlineKeyboardButton::callback("Submit Bid", "basic_auction_bid"),
|
||||
InlineKeyboardButton::callback("Buy It Now", "basic_auction_buy_it_now"),
|
||||
]);
|
||||
}
|
||||
ListingFields::MultiSlotAuction(_) => {
|
||||
keyboard = keyboard.append_row([InlineKeyboardButton::callback(
|
||||
"Submit Bid",
|
||||
"multi_slot_auction_submit_bid",
|
||||
)]);
|
||||
}
|
||||
ListingFields::BlindAuction(_) => {
|
||||
keyboard = keyboard.append_row([InlineKeyboardButton::callback(
|
||||
"Submit Bid",
|
||||
"blind_auction_submit_bid",
|
||||
)]);
|
||||
}
|
||||
};
|
||||
keyboard
|
||||
}
|
||||
|
||||
async fn send_preview_listing_message(
|
||||
bot: &Bot,
|
||||
listing: PersistedListing,
|
||||
from: User,
|
||||
) -> HandlerResult {
|
||||
let mut response_lines = vec![];
|
||||
response_lines.push(format!("<b>{}</b>", &listing.base.title));
|
||||
if let Some(description) = &listing.base.description {
|
||||
response_lines.push(format!("{}", description));
|
||||
}
|
||||
send_message(
|
||||
bot,
|
||||
from,
|
||||
response_lines.join("\n\n"),
|
||||
Some(keyboard_for_listing(&listing)),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_and_listing(
|
||||
db_pool: &SqlitePool,
|
||||
bot: &Bot,
|
||||
user_id: teloxide::types::UserId,
|
||||
listing_id: ListingDbId,
|
||||
target: impl Into<MessageTarget>,
|
||||
) -> HandlerResult<(PersistedUser, PersistedListing)> {
|
||||
let user = match UserDAO::find_by_telegram_id(db_pool, user_id).await? {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
send_message(
|
||||
bot,
|
||||
target,
|
||||
"❌ You don't have an account. Try creating an auction first.",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
return Err(anyhow::anyhow!("User not found"));
|
||||
}
|
||||
};
|
||||
|
||||
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"));
|
||||
}
|
||||
};
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
Ok((user, listing))
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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"),]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -46,7 +46,7 @@ fn test_complete_field_processing_workflow() {
|
||||
|
||||
for (field, input) in workflow {
|
||||
let result = process_field_update(field, &mut draft, input);
|
||||
assert!(result.is_ok(), "Processing {:?} should succeed", field);
|
||||
assert!(result.is_ok(), "Processing {field:?} should succeed");
|
||||
}
|
||||
|
||||
// Verify realistic final state
|
||||
@@ -130,8 +130,7 @@ fn test_natural_language_duration_conversion() {
|
||||
assert_eq!(
|
||||
fields.end_delay,
|
||||
ListingDuration::hours(expected_hours),
|
||||
"Business duration '{}' should convert correctly",
|
||||
input
|
||||
"Business duration '{input}' should convert correctly"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -43,14 +43,11 @@ pub async fn display_listing_summary(
|
||||
.unwrap_or("<i>No description</i>")
|
||||
));
|
||||
|
||||
match &draft.fields {
|
||||
ListingFields::FixedPriceListing(fields) => {
|
||||
response_lines.push(format!(
|
||||
"💰 <b>Buy it Now Price:</b> ${}",
|
||||
fields.buy_now_price
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
if let ListingFields::FixedPriceListing(fields) = &draft.fields {
|
||||
response_lines.push(format!(
|
||||
"💰 <b>Buy it Now Price:</b> ${}",
|
||||
fields.buy_now_price
|
||||
));
|
||||
}
|
||||
|
||||
match &draft.persisted {
|
||||
@@ -73,7 +70,7 @@ pub async fn display_listing_summary(
|
||||
response_lines.push("".to_string());
|
||||
response_lines.push("Please review your listing and choose an action:".to_string());
|
||||
|
||||
send_message(&bot, target, response_lines.join("\n"), keyboard).await?;
|
||||
send_message(bot, target, response_lines.join("\n"), keyboard).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::iter::repeat;
|
||||
|
||||
use sqlx::{prelude::*, query::Query, sqlite::SqliteArguments, Encode, Sqlite};
|
||||
|
||||
@@ -16,15 +15,11 @@ where
|
||||
Box::new(move |query| query.bind(value))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BindFields {
|
||||
binds: Vec<(&'static str, BindFn)>,
|
||||
}
|
||||
|
||||
impl Default for BindFields {
|
||||
fn default() -> Self {
|
||||
Self { binds: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
impl BindFields {
|
||||
#[must_use]
|
||||
@@ -59,6 +54,6 @@ impl BindFields {
|
||||
}
|
||||
|
||||
pub fn bind_placeholders(&self) -> impl Iterator<Item = &'static str> + '_ {
|
||||
repeat("?").take(self.binds.len())
|
||||
std::iter::repeat_n("?", self.binds.len())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ impl UserDAO {
|
||||
.await?;
|
||||
|
||||
let user = FromRow::from_row(&row)?;
|
||||
log::info!("load user from db: {:?}", user);
|
||||
log::info!("load user from db: {user:?}");
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ mod tests {
|
||||
assert_eq!(inserted_user.telegram_id, 12345.into());
|
||||
assert_eq!(inserted_user.username, Some("testuser".to_string()));
|
||||
assert_eq!(inserted_user.first_name, "Test User".to_string());
|
||||
assert_eq!(inserted_user.is_banned, false);
|
||||
assert!(!inserted_user.is_banned);
|
||||
|
||||
// Find by ID
|
||||
let found_user = UserDAO::find_by_id(&pool, inserted_user.persisted.id)
|
||||
@@ -323,7 +323,7 @@ mod tests {
|
||||
|
||||
assert_eq!(updated_user.username, Some("newname".to_string()));
|
||||
assert_eq!(updated_user.first_name, "New Name".to_string());
|
||||
assert_eq!(updated_user.is_banned, true);
|
||||
assert!(updated_user.is_banned);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -203,7 +203,7 @@ mod tests {
|
||||
|
||||
// Insert test data
|
||||
sqlx::query("INSERT INTO test_money (amount, currency) VALUES (?, ?)")
|
||||
.bind(&amount)
|
||||
.bind(amount)
|
||||
.bind(CurrencyType::Usd)
|
||||
.execute(&pool)
|
||||
.await
|
||||
@@ -245,7 +245,7 @@ mod tests {
|
||||
sqlx::query("INSERT INTO test_money (amount, currency, optional_amount) VALUES (?, ?, ?)")
|
||||
.bind(MoneyAmount::from_str("100.00").unwrap())
|
||||
.bind(CurrencyType::Usd)
|
||||
.bind(&optional_amount)
|
||||
.bind(optional_amount)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to insert some optional amount");
|
||||
@@ -290,7 +290,7 @@ mod tests {
|
||||
|
||||
// Insert into database
|
||||
sqlx::query("INSERT INTO test_money (amount, currency) VALUES (?, ?)")
|
||||
.bind(&amount)
|
||||
.bind(amount)
|
||||
.bind(CurrencyType::Usd)
|
||||
.execute(&pool)
|
||||
.await
|
||||
@@ -308,10 +308,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
retrieved_amount.to_string(),
|
||||
expected_str,
|
||||
"Cent-level precision not correct for input: {} (expected: {}, got: {})",
|
||||
input_str,
|
||||
expected_str,
|
||||
retrieved_amount.to_string()
|
||||
"Cent-level precision not correct for input: {input_str} (expected: {expected_str}, got: {retrieved_amount})"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -479,8 +476,7 @@ mod tests {
|
||||
|
||||
let threshold_amount = MoneyAmount::from_str(threshold).unwrap();
|
||||
let query = format!(
|
||||
"SELECT COUNT(*) as count FROM test_bids WHERE bid_amount {} ?",
|
||||
operator
|
||||
"SELECT COUNT(*) as count FROM test_bids WHERE bid_amount {operator} ?"
|
||||
);
|
||||
|
||||
let count_row = sqlx::query(&query)
|
||||
@@ -492,8 +488,7 @@ mod tests {
|
||||
let count: i64 = count_row.get("count");
|
||||
assert_eq!(
|
||||
count as usize, expected_count,
|
||||
"Comparison {} {} failed",
|
||||
operator, threshold
|
||||
"Comparison {operator} {threshold} failed"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -524,8 +519,7 @@ mod tests {
|
||||
let count: i64 = count_row.get("count");
|
||||
assert_eq!(
|
||||
count as usize, expected_count,
|
||||
"BETWEEN {} AND {} failed",
|
||||
min_amount, max_amount
|
||||
"BETWEEN {min_amount} AND {max_amount} failed"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,11 +94,11 @@ mod tests {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum InnerEnum {
|
||||
InnerSimple,
|
||||
InnerWithParam(&'static str),
|
||||
InnerWithMultiple(&'static str, i32),
|
||||
InnerWithStruct { field: &'static str },
|
||||
InnerWithMultiStruct { field: &'static str, number: i32 },
|
||||
Simple,
|
||||
WithParam(&'static str),
|
||||
WithMultiple(&'static str, i32),
|
||||
WithStruct { field: &'static str },
|
||||
WithMultiStruct { field: &'static str, number: i32 },
|
||||
}
|
||||
|
||||
// Helper function for testing handlers with expected results
|
||||
@@ -144,29 +144,29 @@ mod tests {
|
||||
)]
|
||||
// Single parameter extraction from nested enum
|
||||
#[case::nested_single_match(
|
||||
case![TestEnum::NestedVariant(InnerEnum::InnerWithParam(p))].endpoint(|p: &'static str| async move { p }),
|
||||
TestEnum::NestedVariant(InnerEnum::InnerWithParam("nested")),
|
||||
case![TestEnum::NestedVariant(InnerEnum::WithParam(p))].endpoint(|p: &'static str| async move { p }),
|
||||
TestEnum::NestedVariant(InnerEnum::WithParam("nested")),
|
||||
Some("nested")
|
||||
)]
|
||||
#[case::nested_single_wrong_inner_variant(
|
||||
case![TestEnum::NestedVariant(InnerEnum::InnerWithParam(p))].endpoint(|p: &'static str| async move { p }),
|
||||
TestEnum::NestedVariant(InnerEnum::InnerSimple),
|
||||
case![TestEnum::NestedVariant(InnerEnum::WithParam(p))].endpoint(|p: &'static str| async move { p }),
|
||||
TestEnum::NestedVariant(InnerEnum::Simple),
|
||||
None
|
||||
)]
|
||||
#[case::nested_single_wrong_outer_variant(
|
||||
case![TestEnum::NestedVariant(InnerEnum::InnerWithParam(p))].endpoint(|p: &'static str| async move { p }),
|
||||
case![TestEnum::NestedVariant(InnerEnum::WithParam(p))].endpoint(|p: &'static str| async move { p }),
|
||||
TestEnum::DefaultVariant,
|
||||
None
|
||||
)]
|
||||
// Single field extraction from nested struct
|
||||
#[case::struct_field_match(
|
||||
case![TestEnum::NestedVariant(InnerEnum::InnerWithStruct { field })].endpoint(|field: &'static str| async move { field }),
|
||||
TestEnum::NestedVariant(InnerEnum::InnerWithStruct { field: "struct_value" }),
|
||||
case![TestEnum::NestedVariant(InnerEnum::WithStruct { field })].endpoint(|field: &'static str| async move { field }),
|
||||
TestEnum::NestedVariant(InnerEnum::WithStruct { field: "struct_value" }),
|
||||
Some("struct_value")
|
||||
)]
|
||||
#[case::struct_field_no_match(
|
||||
case![TestEnum::NestedVariant(InnerEnum::InnerWithStruct { field })].endpoint(|field: &'static str| async move { field }),
|
||||
TestEnum::NestedVariant(InnerEnum::InnerSimple),
|
||||
case![TestEnum::NestedVariant(InnerEnum::WithStruct { field })].endpoint(|field: &'static str| async move { field }),
|
||||
TestEnum::NestedVariant(InnerEnum::Simple),
|
||||
None
|
||||
)]
|
||||
#[tokio::test]
|
||||
@@ -202,8 +202,8 @@ mod tests {
|
||||
|
||||
// Test cases for nested multiple parameter extraction
|
||||
#[rstest::rstest]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::InnerWithMultiple("nested", 123)), Some(("nested", 123)))]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::InnerSimple), None)]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::WithMultiple("nested", 123)), Some(("nested", 123)))]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::Simple), None)]
|
||||
#[case(TestEnum::DefaultVariant, None)]
|
||||
#[tokio::test]
|
||||
async fn test_nested_multiple_parameter_extraction(
|
||||
@@ -211,7 +211,7 @@ mod tests {
|
||||
#[case] expected_params: Option<(&'static str, i32)>,
|
||||
) {
|
||||
let handler: Handler<'static, (&str, i32), DpHandlerDescription> =
|
||||
case![TestEnum::NestedVariant(InnerEnum::InnerWithMultiple(s, n))]
|
||||
case![TestEnum::NestedVariant(InnerEnum::WithMultiple(s, n))]
|
||||
.endpoint(|params: (&'static str, i32)| async move { params });
|
||||
|
||||
let input = deps![input_variant];
|
||||
@@ -225,8 +225,8 @@ mod tests {
|
||||
|
||||
// Test cases for struct pattern extraction
|
||||
#[rstest::rstest]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::InnerWithStruct { field: "struct_field" }), Some("struct_field"))]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::InnerSimple), None)]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::WithStruct { field: "struct_field" }), Some("struct_field"))]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::Simple), None)]
|
||||
#[case(TestEnum::DefaultVariant, None)]
|
||||
#[tokio::test]
|
||||
async fn test_struct_pattern_extraction(
|
||||
@@ -234,10 +234,8 @@ mod tests {
|
||||
#[case] expected_field: Option<&'static str>,
|
||||
) {
|
||||
let handler: Handler<'static, &str, DpHandlerDescription> =
|
||||
case![TestEnum::NestedVariant(InnerEnum::InnerWithStruct {
|
||||
field
|
||||
})]
|
||||
.endpoint(|field: &'static str| async move { field });
|
||||
case![TestEnum::NestedVariant(InnerEnum::WithStruct { field })]
|
||||
.endpoint(|field: &'static str| async move { field });
|
||||
|
||||
let input = deps![input_variant];
|
||||
let result = handler.dispatch(input).await;
|
||||
@@ -250,8 +248,8 @@ mod tests {
|
||||
|
||||
// Test cases for multi-field struct pattern extraction
|
||||
#[rstest::rstest]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::InnerWithMultiStruct { field: "multi_field", number: 42 }), Some(("multi_field", 42)))]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::InnerSimple), None)]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::WithMultiStruct { field: "multi_field", number: 42 }), Some(("multi_field", 42)))]
|
||||
#[case(TestEnum::NestedVariant(InnerEnum::Simple), None)]
|
||||
#[case(TestEnum::DefaultVariant, None)]
|
||||
#[tokio::test]
|
||||
async fn test_multi_struct_pattern_extraction(
|
||||
@@ -259,7 +257,7 @@ mod tests {
|
||||
#[case] expected_fields: Option<(&'static str, i32)>,
|
||||
) {
|
||||
let handler: Handler<'static, (&str, i32), DpHandlerDescription> =
|
||||
case![TestEnum::NestedVariant(InnerEnum::InnerWithMultiStruct {
|
||||
case![TestEnum::NestedVariant(InnerEnum::WithMultiStruct {
|
||||
field,
|
||||
number
|
||||
})]
|
||||
|
||||
@@ -41,6 +41,20 @@ macro_rules! keyboard_buttons {
|
||||
$($($name::$variant => teloxide::types::InlineKeyboardButton::callback($text, $callback_data)),*),*
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn to_switch_inline_query(self) -> teloxide::types::InlineKeyboardButton {
|
||||
match self {
|
||||
$($($name::$variant => teloxide::types::InlineKeyboardButton::switch_inline_query($text, $callback_data)),*),*
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn title(self) -> &'static str {
|
||||
match self {
|
||||
$($($name::$variant => $text),*),*
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<$name> for teloxide::types::InlineKeyboardButton {
|
||||
fn from(value: $name) -> Self {
|
||||
@@ -50,13 +64,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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
src/main.rs
35
src/main.rs
@@ -9,7 +9,7 @@ mod sqlite_storage;
|
||||
mod test_utils;
|
||||
|
||||
use crate::commands::{
|
||||
my_listings::{my_listings_handler, MyListingsState},
|
||||
my_listings::{my_listings_handler, my_listings_inline_handler, MyListingsState},
|
||||
new_listing::{new_listing_handler, NewListingState},
|
||||
};
|
||||
use crate::sqlite_storage::SqliteStorage;
|
||||
@@ -62,6 +62,7 @@ pub enum Command {
|
||||
enum DialogueRootState {
|
||||
#[default]
|
||||
Start,
|
||||
MainMenu,
|
||||
NewListing(NewListingState),
|
||||
MyListings(MyListingsState),
|
||||
}
|
||||
@@ -88,18 +89,28 @@ async fn main() -> Result<()> {
|
||||
Dispatcher::builder(
|
||||
bot,
|
||||
dptree::entry()
|
||||
.enter_dialogue::<Update, SqliteStorage<Json>, DialogueRootState>()
|
||||
.branch(new_listing_handler())
|
||||
.branch(my_listings_handler())
|
||||
.branch(my_listings_inline_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::MyBids].endpoint(handle_my_bids))
|
||||
.branch(dptree::case![Command::Settings].endpoint(handle_settings)),
|
||||
),
|
||||
dptree::entry()
|
||||
.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()
|
||||
.filter_command::<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(Update::filter_message().endpoint(unknown_message_handler)),
|
||||
)
|
||||
|
||||
@@ -143,15 +143,6 @@ pub fn create_single_button_keyboard(text: &str, callback_data: &str) -> InlineK
|
||||
InlineKeyboardMarkup::new([[InlineKeyboardButton::callback(text, callback_data)]])
|
||||
}
|
||||
|
||||
// Create a keyboard with multiple buttons in a single row
|
||||
pub fn create_single_row_keyboard(buttons: &[(&str, &str)]) -> InlineKeyboardMarkup {
|
||||
let keyboard_buttons: Vec<InlineKeyboardButton> = buttons
|
||||
.iter()
|
||||
.map(|(text, callback_data)| InlineKeyboardButton::callback(*text, *callback_data))
|
||||
.collect();
|
||||
InlineKeyboardMarkup::new([keyboard_buttons])
|
||||
}
|
||||
|
||||
// Create a keyboard with multiple rows
|
||||
pub fn create_multi_row_keyboard(rows: &[&[(&str, &str)]]) -> InlineKeyboardMarkup {
|
||||
let mut keyboard = InlineKeyboardMarkup::default();
|
||||
|
||||
Reference in New Issue
Block a user