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::{
|
use crate::{
|
||||||
case,
|
case,
|
||||||
commands::{
|
commands::{
|
||||||
enter_main_menu,
|
enter_main_menu,
|
||||||
|
my_listings::keyboard::{ManageListingButtons, MyListingsButtons},
|
||||||
new_listing::{enter_edit_listing_draft, ListingDraft},
|
new_listing::{enter_edit_listing_draft, ListingDraft},
|
||||||
},
|
},
|
||||||
db::{listing::PersistedListing, user::PersistedUser, ListingDAO, ListingDbId, UserDAO},
|
db::{
|
||||||
keyboard_buttons,
|
listing::{ListingFields, PersistedListing},
|
||||||
|
user::PersistedUser,
|
||||||
|
ListingDAO, ListingDbId, UserDAO,
|
||||||
|
},
|
||||||
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
|
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
|
||||||
Command, DialogueRootState, HandlerResult, RootDialogue,
|
Command, DialogueRootState, HandlerResult, RootDialogue,
|
||||||
};
|
};
|
||||||
|
use log::info;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use teloxide::{
|
use teloxide::{
|
||||||
dispatching::{DpHandlerDescription, UpdateFilterExt},
|
dispatching::{DpHandlerDescription, UpdateFilterExt},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
types::{InlineKeyboardButton, Message},
|
types::{
|
||||||
|
InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResult, InlineQueryResultArticle,
|
||||||
|
InputMessageContent, InputMessageContentText, Message, ParseMode, User,
|
||||||
|
},
|
||||||
Bot,
|
Bot,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,22 +39,11 @@ impl From<MyListingsState> for DialogueRootState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keyboard_buttons! {
|
pub fn my_listings_inline_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
||||||
enum ManageListingButtons {
|
Update::filter_inline_query()
|
||||||
[
|
.inspect(|query: InlineQuery| info!("Received inline query: {:?}", query))
|
||||||
Edit("✏️ Edit", "manage_listing_edit"),
|
.filter_map_async(inline_query_extract_forward_listing)
|
||||||
Delete("🗑️ Delete", "manage_listing_delete"),
|
.endpoint(handle_forward_listing)
|
||||||
],
|
|
||||||
[
|
|
||||||
Back("⬅️ Back", "manage_listing_back"),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboard_buttons! {
|
|
||||||
enum MyListingsButtons {
|
|
||||||
BackToMenu("⬅️ Back to Menu", "my_listings_back_to_menu"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn my_listings_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
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(
|
async fn handle_my_listings_command_input(
|
||||||
db_pool: SqlitePool,
|
db_pool: SqlitePool,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
@@ -202,7 +314,21 @@ async fn show_listing_details(
|
|||||||
bot,
|
bot,
|
||||||
target,
|
target,
|
||||||
response,
|
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?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -219,6 +345,14 @@ async fn handle_managing_listing_callback(
|
|||||||
let target = (from.clone(), message_id);
|
let target = (from.clone(), message_id);
|
||||||
|
|
||||||
match ManageListingButtons::try_from(data.as_str())? {
|
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 => {
|
ManageListingButtons::Edit => {
|
||||||
let (_, listing) =
|
let (_, listing) =
|
||||||
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
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(())
|
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(
|
async fn get_user_and_listing(
|
||||||
db_pool: &SqlitePool,
|
db_pool: &SqlitePool,
|
||||||
bot: &Bot,
|
bot: &Bot,
|
||||||
@@ -41,6 +41,20 @@ macro_rules! keyboard_buttons {
|
|||||||
$($($name::$variant => teloxide::types::InlineKeyboardButton::callback($text, $callback_data)),*),*
|
$($($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 {
|
impl From<$name> for teloxide::types::InlineKeyboardButton {
|
||||||
fn from(value: $name) -> Self {
|
fn from(value: $name) -> Self {
|
||||||
|
|||||||
37
src/main.rs
37
src/main.rs
@@ -9,7 +9,7 @@ mod sqlite_storage;
|
|||||||
mod test_utils;
|
mod test_utils;
|
||||||
|
|
||||||
use crate::commands::{
|
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},
|
new_listing::{new_listing_handler, NewListingState},
|
||||||
};
|
};
|
||||||
use crate::sqlite_storage::SqliteStorage;
|
use crate::sqlite_storage::SqliteStorage;
|
||||||
@@ -89,21 +89,28 @@ async fn main() -> Result<()> {
|
|||||||
Dispatcher::builder(
|
Dispatcher::builder(
|
||||||
bot,
|
bot,
|
||||||
dptree::entry()
|
dptree::entry()
|
||||||
.enter_dialogue::<Update, SqliteStorage<Json>, DialogueRootState>()
|
.branch(my_listings_inline_handler())
|
||||||
.branch(new_listing_handler())
|
|
||||||
.branch(my_listings_handler())
|
|
||||||
.branch(Update::filter_callback_query().branch(
|
|
||||||
dptree::case![DialogueRootState::MainMenu].endpoint(handle_main_menu_callback),
|
|
||||||
))
|
|
||||||
.branch(
|
.branch(
|
||||||
Update::filter_message().branch(
|
dptree::entry()
|
||||||
dptree::entry()
|
.enter_dialogue::<Update, SqliteStorage<Json>, DialogueRootState>()
|
||||||
.filter_command::<Command>()
|
.branch(new_listing_handler())
|
||||||
.branch(dptree::case![Command::Start].endpoint(handle_start))
|
.branch(my_listings_handler())
|
||||||
.branch(dptree::case![Command::Help].endpoint(handle_help))
|
.branch(
|
||||||
.branch(dptree::case![Command::MyBids].endpoint(handle_my_bids))
|
Update::filter_callback_query().branch(
|
||||||
.branch(dptree::case![Command::Settings].endpoint(handle_settings)),
|
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)),
|
.branch(Update::filter_message().endpoint(unknown_message_handler)),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user