451 lines
15 KiB
Rust
451 lines
15 KiB
Rust
mod keyboard;
|
|
|
|
use std::ops::Deref;
|
|
|
|
use crate::{
|
|
case,
|
|
commands::{
|
|
enter_main_menu,
|
|
my_listings::keyboard::{ManageListingButtons, MyListingsButtons},
|
|
new_listing::{
|
|
enter_edit_listing_draft, enter_select_new_listing_type, keyboard::NavKeyboardButtons,
|
|
messages::steps_for_listing_type, ListingDraft,
|
|
},
|
|
},
|
|
db::{
|
|
listing::{ListingFields, PersistedListing},
|
|
user::PersistedUser,
|
|
DAOs, DbListingId, ListingType,
|
|
},
|
|
handle_error::with_error_handler,
|
|
handler_utils::{find_listing_by_id, find_or_create_db_user_from_update},
|
|
message_utils::{extract_callback_data, pluralize_with_count},
|
|
start_command_data::StartCommandData,
|
|
App, BotError, BotResult, Command, DialogueRootState, RootDialogue,
|
|
};
|
|
use anyhow::Context;
|
|
use log::info;
|
|
use serde::{Deserialize, Serialize};
|
|
use teloxide::{
|
|
dispatching::{DpHandlerDescription, UpdateFilterExt},
|
|
prelude::*,
|
|
types::{
|
|
InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResult, InlineQueryResultArticle,
|
|
InputMessageContent, InputMessageContentText, ParseMode, User,
|
|
},
|
|
};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum MyListingsState {
|
|
ViewingListings,
|
|
ManagingListing(DbListingId),
|
|
}
|
|
impl From<MyListingsState> for DialogueRootState {
|
|
fn from(state: MyListingsState) -> Self {
|
|
DialogueRootState::MyListings(state)
|
|
}
|
|
}
|
|
|
|
pub fn my_listings_inline_handler() -> Handler<'static, BotResult, DpHandlerDescription> {
|
|
Update::filter_inline_query()
|
|
.filter_map_async(inline_query_extract_forward_listing)
|
|
.endpoint(handle_forward_listing)
|
|
}
|
|
|
|
pub fn my_listings_handler() -> Handler<'static, BotResult, DpHandlerDescription> {
|
|
dptree::entry()
|
|
.branch(
|
|
Update::filter_message()
|
|
.filter_map(StartCommandData::get_from_update)
|
|
.filter_map(StartCommandData::get_view_listing_details_as_buyer_start_command)
|
|
.filter_map_async(find_listing_by_id)
|
|
.endpoint(with_error_handler(handle_view_listing_details)),
|
|
)
|
|
.branch(
|
|
Update::filter_message().filter_command::<Command>().branch(
|
|
dptree::case![Command::MyListings]
|
|
.filter_map_async(find_or_create_db_user_from_update)
|
|
.endpoint(with_error_handler(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(with_error_handler(handle_viewing_listings_callback)),
|
|
)
|
|
.branch(
|
|
case![DialogueRootState::MyListings(
|
|
MyListingsState::ManagingListing(listing_id)
|
|
)]
|
|
.endpoint(with_error_handler(handle_managing_listing_callback)),
|
|
),
|
|
)
|
|
}
|
|
|
|
async fn handle_view_listing_details(app: App, listing: PersistedListing) -> BotResult {
|
|
send_listing_details_message(app, listing, None).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn inline_query_extract_forward_listing(
|
|
app: App,
|
|
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 = DbListingId::new(listing_id_str.parse::<i64>().ok()?);
|
|
let listing = app
|
|
.daos
|
|
.listing
|
|
.find_by_id(listing_id)
|
|
.await
|
|
.unwrap_or(None)?;
|
|
Some(listing)
|
|
}
|
|
|
|
async fn handle_forward_listing(
|
|
app: App,
|
|
inline_query: InlineQuery,
|
|
listing: PersistedListing,
|
|
) -> BotResult {
|
|
info!("Handling forward listing inline query for listing {listing:?}");
|
|
|
|
// Create inline keyboard with auction interaction buttons
|
|
let keyboard = InlineKeyboardMarkup::default()
|
|
.append_row([
|
|
InlineKeyboardButton::url(
|
|
"💰 Place Bid",
|
|
app.url_for_start_command(StartCommandData::PlaceBidOnListing(
|
|
listing.persisted.id,
|
|
)),
|
|
),
|
|
InlineKeyboardButton::callback(
|
|
"👀 Watch",
|
|
format!("inline_watch:{}", listing.persisted.id),
|
|
),
|
|
])
|
|
.append_row([InlineKeyboardButton::url(
|
|
"🔗 View Full Details",
|
|
app.url_for_start_command(StartCommandData::ViewListingDetailsAsBuyer(
|
|
listing.persisted.id,
|
|
)),
|
|
)]);
|
|
|
|
// 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.base.ends_at.format("%b %d, %Y at %H:%M UTC")
|
|
);
|
|
|
|
app.bot
|
|
.answer_inline_query(
|
|
inline_query.id,
|
|
vec![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
|
|
.map_err(|e| anyhow::anyhow!("Error answering inline query: {e:?}"))?;
|
|
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(
|
|
app: App,
|
|
dialogue: RootDialogue,
|
|
user: PersistedUser,
|
|
) -> BotResult {
|
|
enter_my_listings(app, dialogue, user, None).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn enter_my_listings(
|
|
app: App,
|
|
dialogue: RootDialogue,
|
|
user: PersistedUser,
|
|
flash: Option<String>,
|
|
) -> BotResult {
|
|
// Transition to ViewingListings state
|
|
dialogue
|
|
.update(MyListingsState::ViewingListings)
|
|
.await
|
|
.context("failed to update dialogue")?;
|
|
|
|
let listings = app.daos.listing.find_by_seller(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(),
|
|
NavKeyboardButtons::Back.to_button(),
|
|
]);
|
|
|
|
if listings.is_empty() {
|
|
app.bot
|
|
.send_html_message(
|
|
"📋 <b>My Listings</b>\n\n\
|
|
You don't have any listings yet."
|
|
.to_string(),
|
|
Some(keyboard),
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
|
|
let mut 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")
|
|
);
|
|
|
|
if let Some(flash) = flash {
|
|
response = format!("{flash}\n\n{response}");
|
|
}
|
|
|
|
app.bot.send_html_message(response, Some(keyboard)).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_viewing_listings_callback(
|
|
app: App,
|
|
dialogue: RootDialogue,
|
|
callback_query: CallbackQuery,
|
|
user: PersistedUser,
|
|
) -> BotResult {
|
|
let data = extract_callback_data(app.bot.deref(), callback_query).await?;
|
|
|
|
if let Ok(NavKeyboardButtons::Back) = NavKeyboardButtons::try_from(data.as_str()) {
|
|
return enter_main_menu(app, dialogue).await;
|
|
}
|
|
|
|
// 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(&app.daos, user, listing_id).await?;
|
|
enter_show_listing_details(app, dialogue, listing).await?;
|
|
}
|
|
MyListingsButtons::NewListing => {
|
|
enter_select_new_listing_type(app, dialogue).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn enter_show_listing_details(
|
|
app: App,
|
|
dialogue: RootDialogue,
|
|
listing: PersistedListing,
|
|
) -> BotResult {
|
|
let listing_id = listing.persisted.id;
|
|
dialogue
|
|
.update(MyListingsState::ManagingListing(listing_id))
|
|
.await
|
|
.context("failed to update dialogue")?;
|
|
let keyboard = InlineKeyboardMarkup::default()
|
|
.append_row([
|
|
ManageListingButtons::PreviewMessage.to_button(),
|
|
InlineKeyboardButton::switch_inline_query(
|
|
ManageListingButtons::ForwardListing.title(),
|
|
format!("forward_listing:{listing_id}"),
|
|
),
|
|
])
|
|
.append_row([
|
|
ManageListingButtons::Edit.to_button(),
|
|
ManageListingButtons::Delete.to_button(),
|
|
])
|
|
.append_row([ManageListingButtons::Back.to_button()]);
|
|
send_listing_details_message(app, listing, Some(keyboard)).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn send_listing_details_message(
|
|
app: App,
|
|
listing: PersistedListing,
|
|
keyboard: Option<InlineKeyboardMarkup>,
|
|
) -> BotResult {
|
|
let listing_type = Into::<ListingType>::into(&listing.fields);
|
|
let mut response_lines = vec![format!("🔍 <b>{listing_type} Details</b>")];
|
|
response_lines.push("".to_string());
|
|
|
|
let draft = ListingDraft::from_persisted(listing);
|
|
for step in steps_for_listing_type(listing_type) {
|
|
let field_value = match (step.get_field_value)(&draft) {
|
|
Ok(value) => value.unwrap_or_else(|| "(none)".to_string()),
|
|
Err(_) => continue,
|
|
};
|
|
response_lines.push(format!("<b>{}:</b> {}", step.field_name, field_value));
|
|
}
|
|
app.bot
|
|
.send_html_message(response_lines.join("\n"), keyboard)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_managing_listing_callback(
|
|
app: App,
|
|
dialogue: RootDialogue,
|
|
callback_query: CallbackQuery,
|
|
user: PersistedUser,
|
|
listing_id: DbListingId,
|
|
) -> BotResult {
|
|
let from = callback_query.from.clone();
|
|
let data = extract_callback_data(&app.bot, callback_query).await?;
|
|
|
|
match ManageListingButtons::try_from(data.as_str())? {
|
|
ManageListingButtons::PreviewMessage => {
|
|
let listing = app
|
|
.daos
|
|
.listing
|
|
.find_by_id(listing_id)
|
|
.await?
|
|
.ok_or(anyhow::anyhow!("Listing not found"))?;
|
|
send_preview_listing_message(app, listing, from).await?;
|
|
}
|
|
ManageListingButtons::ForwardListing => {
|
|
unimplemented!("Forward listing not implemented");
|
|
}
|
|
ManageListingButtons::Edit => {
|
|
let listing = get_listing_for_user(&app.daos, user, listing_id).await?;
|
|
let draft = ListingDraft::from_persisted(listing);
|
|
enter_edit_listing_draft(app, draft, dialogue, None).await?;
|
|
}
|
|
ManageListingButtons::Delete => {
|
|
app.daos.listing.delete_listing(listing_id).await?;
|
|
enter_my_listings(app, dialogue, user, Some("Listing deleted.".to_string())).await?;
|
|
}
|
|
ManageListingButtons::Back => {
|
|
enter_my_listings(app, dialogue, user, None).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(
|
|
app: App,
|
|
listing: PersistedListing,
|
|
from: User,
|
|
) -> BotResult {
|
|
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());
|
|
}
|
|
|
|
app.bot
|
|
.with_target(from.into())
|
|
.send_html_message(
|
|
response_lines.join("\n\n"),
|
|
Some(keyboard_for_listing(&listing)),
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_listing_for_user(
|
|
daos: &DAOs,
|
|
user: PersistedUser,
|
|
listing_id: DbListingId,
|
|
) -> BotResult<PersistedListing> {
|
|
let listing = match daos.listing.find_by_id(listing_id).await? {
|
|
Some(listing) => listing,
|
|
None => {
|
|
return Err(BotError::UserVisibleError("❌ Listing not found.".into()));
|
|
}
|
|
};
|
|
|
|
if listing.base.seller_id != user.persisted.id {
|
|
return Err(BotError::UserVisibleError(
|
|
"❌ You can only manage your own auctions.".into(),
|
|
));
|
|
}
|
|
|
|
Ok(listing)
|
|
}
|