- Update my_listings command structure and keyboard handling - Enhance new_listing workflow with improved callbacks and handlers - Refactor database user model and keyboard utilities - Add new handler utilities module - Update main bot configuration and start command
439 lines
14 KiB
Rust
439 lines
14 KiB
Rust
mod keyboard;
|
|
|
|
use crate::{
|
|
case,
|
|
commands::{
|
|
enter_main_menu,
|
|
my_listings::keyboard::{ManageListingButtons, MyListingsButtons},
|
|
new_listing::{enter_edit_listing_draft, enter_select_new_listing_type, ListingDraft},
|
|
},
|
|
db::{
|
|
listing::{ListingFields, PersistedListing},
|
|
user::PersistedUser,
|
|
ListingDAO, ListingDbId,
|
|
},
|
|
handler_utils::{
|
|
find_or_create_db_user_from_callback_query, find_or_create_db_user_from_message,
|
|
},
|
|
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()
|
|
.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]
|
|
.filter_map_async(find_or_create_db_user_from_message)
|
|
.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
|
|
)]
|
|
.filter_map_async(find_or_create_db_user_from_callback_query)
|
|
.endpoint(handle_viewing_listings_callback),
|
|
)
|
|
.branch(
|
|
case![DialogueRootState::MyListings(
|
|
MyListingsState::ManagingListing(listing_id)
|
|
)]
|
|
.filter_map_async(find_or_create_db_user_from_callback_query)
|
|
.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,
|
|
user: PersistedUser,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
enter_my_listings(db_pool, bot, dialogue, user, msg.chat).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn enter_my_listings(
|
|
db_pool: SqlitePool,
|
|
bot: Bot,
|
|
dialogue: RootDialogue,
|
|
user: PersistedUser,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
// Transition to ViewingListings state
|
|
dialogue.update(MyListingsState::ViewingListings).await?;
|
|
|
|
let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?;
|
|
// Create keyboard with buttons for each listing
|
|
let mut keyboard = teloxide::types::InlineKeyboardMarkup::default();
|
|
for listing in &listings {
|
|
keyboard = keyboard.append_row(vec![MyListingsButtons::listing_into_button(listing)]);
|
|
}
|
|
keyboard = keyboard.append_row(vec![
|
|
MyListingsButtons::new_listing_into_button(),
|
|
MyListingsButtons::back_to_menu_into_button(),
|
|
]);
|
|
|
|
if listings.is_empty() {
|
|
send_message(
|
|
&bot,
|
|
target,
|
|
"📋 <b>My Listings</b>\n\n\
|
|
You don't have any listings yet.",
|
|
Some(keyboard),
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
|
|
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,
|
|
user: PersistedUser,
|
|
) -> 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
|
|
let button = MyListingsButtons::try_from(data.as_str())?;
|
|
|
|
match button {
|
|
MyListingsButtons::SelectListing(listing_id) => {
|
|
let listing =
|
|
get_listing_for_user(&db_pool, &bot, user, listing_id, target.clone()).await?;
|
|
dialogue
|
|
.update(MyListingsState::ManagingListing(listing_id))
|
|
.await?;
|
|
show_listing_details(&bot, listing, target).await?;
|
|
}
|
|
MyListingsButtons::NewListing => {
|
|
enter_select_new_listing_type(bot, dialogue, target).await?;
|
|
}
|
|
MyListingsButtons::BackToMenu => {
|
|
// Transition back to main menu using the reusable function
|
|
enter_main_menu(bot, dialogue, 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,
|
|
user: PersistedUser,
|
|
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 = ListingDAO::find_by_id(&db_pool, listing_id)
|
|
.await?
|
|
.ok_or(anyhow::anyhow!("Listing not found"))?;
|
|
send_preview_listing_message(&bot, listing, from).await?;
|
|
}
|
|
ManageListingButtons::ForwardListing => {
|
|
unimplemented!("Forward listing not implemented");
|
|
}
|
|
ManageListingButtons::Edit => {
|
|
let listing =
|
|
get_listing_for_user(&db_pool, &bot, user, 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?;
|
|
enter_my_listings(db_pool, bot, dialogue, user, 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(description.to_owned());
|
|
}
|
|
send_message(
|
|
bot,
|
|
from,
|
|
response_lines.join("\n\n"),
|
|
Some(keyboard_for_listing(&listing)),
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_listing_for_user(
|
|
db_pool: &SqlitePool,
|
|
bot: &Bot,
|
|
user: PersistedUser,
|
|
listing_id: ListingDbId,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult<PersistedListing> {
|
|
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(listing)
|
|
}
|