feat: Add inline keyboard support to auction listing inline queries
- Add interactive inline keyboard with bid, watch, and share buttons - Implement separate inline query handler before dialogue system - Fix dispatcher structure to handle both inline queries and regular messages - Add dynamic bot username fetching for deep links - Support rich formatted auction listing messages with pricing info - Handle forward_listing:ID pattern for sharing specific auctions Resolves inline query handling and adds interactive auction sharing
This commit is contained in:
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"),]
|
||||
}
|
||||
}
|
||||
@@ -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<MyListingsState> 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<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,
|
||||
@@ -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!("<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,
|
||||
@@ -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 {
|
||||
|
||||
37
src/main.rs
37
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::<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(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)),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user