diff --git a/src/commands/my_listings/keyboard.rs b/src/commands/my_listings/keyboard.rs new file mode 100644 index 0000000..1dde374 --- /dev/null +++ b/src/commands/my_listings/keyboard.rs @@ -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"),] + } +} diff --git a/src/commands/my_listings.rs b/src/commands/my_listings/mod.rs similarity index 52% rename from src/commands/my_listings.rs rename to src/commands/my_listings/mod.rs index 1a8a9bb..39ea684 100644 --- a/src/commands/my_listings.rs +++ b/src/commands/my_listings/mod.rs @@ -1,20 +1,30 @@ +mod keyboard; + use crate::{ case, commands::{ enter_main_menu, + my_listings::keyboard::{ManageListingButtons, MyListingsButtons}, new_listing::{enter_edit_listing_draft, ListingDraft}, }, - db::{listing::PersistedListing, user::PersistedUser, ListingDAO, ListingDbId, UserDAO}, - keyboard_buttons, + 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, Message}, + types::{ + InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResult, InlineQueryResultArticle, + InputMessageContent, InputMessageContentText, Message, ParseMode, User, + }, Bot, }; @@ -29,22 +39,11 @@ impl From for DialogueRootState { } } -keyboard_buttons! { - enum ManageListingButtons { - [ - Edit("✏️ Edit", "manage_listing_edit"), - Delete("🗑️ Delete", "manage_listing_delete"), - ], - [ - Back("⬅️ Back", "manage_listing_back"), - ] - } -} - -keyboard_buttons! { - enum MyListingsButtons { - BackToMenu("⬅️ Back to Menu", "my_listings_back_to_menu"), - } +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> { @@ -72,6 +71,119 @@ pub fn my_listings_handler() -> Handler<'static, HandlerResult, DpHandlerDescrip ) } +async fn inline_query_extract_forward_listing( + db_pool: SqlitePool, + inline_query: InlineQuery, +) -> Option { + 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::().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!( + "🎯 {}\n\n\ + 📝 {}\n\n\ + 💰 Current Price: ${}\n\ + ⏰ Ends: {}\n\n\ + Use the buttons below to interact! ⬇️", + 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, @@ -202,7 +314,21 @@ async fn show_listing_details( bot, target, response, - Some(ManageListingButtons::to_keyboard()), + 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(()) @@ -219,6 +345,14 @@ async fn handle_managing_listing_callback( 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?; @@ -238,6 +372,57 @@ async fn handle_managing_listing_callback( 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!("{}", &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, diff --git a/src/keyboard_utils.rs b/src/keyboard_utils.rs index 7d567d0..18dc716 100644 --- a/src/keyboard_utils.rs +++ b/src/keyboard_utils.rs @@ -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 { diff --git a/src/main.rs b/src/main.rs index eae9c84..673793b 100644 --- a/src/main.rs +++ b/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; @@ -89,21 +89,28 @@ async fn main() -> Result<()> { Dispatcher::builder( bot, dptree::entry() - .enter_dialogue::, 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(my_listings_inline_handler()) .branch( - Update::filter_message().branch( - dptree::entry() - .filter_command::() - .branch(dptree::case![Command::Start].endpoint(handle_start)) - .branch(dptree::case![Command::Help].endpoint(handle_help)) - .branch(dptree::case![Command::MyBids].endpoint(handle_my_bids)) - .branch(dptree::case![Command::Settings].endpoint(handle_settings)), - ), + dptree::entry() + .enter_dialogue::, 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::() + .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)), )