Files
pawctioneer-bot/src/commands/my_listings.rs
Dylan Knutson c9a824dd88 cargo clippy
2025-08-30 05:50:44 +00:00

283 lines
8.5 KiB
Rust

use crate::{
case,
commands::{
enter_main_menu,
new_listing::{enter_edit_listing_draft, ListingDraft},
},
db::{listing::PersistedListing, user::PersistedUser, ListingDAO, ListingDbId, UserDAO},
keyboard_buttons,
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
Command, DialogueRootState, HandlerResult, RootDialogue,
};
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use teloxide::{
dispatching::{DpHandlerDescription, UpdateFilterExt},
prelude::*,
types::{InlineKeyboardButton, Message},
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)
}
}
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_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
dptree::entry()
.branch(
Update::filter_message().filter_command::<Command>().branch(
dptree::case![Command::MyListings].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
)]
.endpoint(handle_viewing_listings_callback),
)
.branch(
case![DialogueRootState::MyListings(
MyListingsState::ManagingListing(listing_id)
)]
.endpoint(handle_managing_listing_callback),
),
)
}
async fn handle_my_listings_command_input(
db_pool: SqlitePool,
bot: Bot,
dialogue: RootDialogue,
msg: Message,
) -> HandlerResult {
let from = msg.from.unwrap();
show_listings_for_user(db_pool, dialogue, bot, from.id, msg.chat).await?;
Ok(())
}
pub async fn show_listings_for_user(
db_pool: SqlitePool,
dialogue: RootDialogue,
bot: Bot,
user: teloxide::types::UserId,
target: impl Into<MessageTarget>,
) -> HandlerResult {
// If we reach here, show the listings menu
let user = match UserDAO::find_by_telegram_id(&db_pool, user).await? {
Some(user) => user,
None => {
send_message(
&bot,
target,
"You don't have an account. Try creating an auction first.",
None,
)
.await?;
return Err(anyhow::anyhow!("User not found"));
}
};
// Transition to ViewingListings state
dialogue.update(MyListingsState::ViewingListings).await?;
let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?;
if listings.is_empty() {
// Create keyboard with just the back button
let keyboard =
teloxide::types::InlineKeyboardMarkup::new([[MyListingsButtons::BackToMenu.into()]]);
send_message(
&bot,
target,
"📋 <b>My Listings</b>\n\n\
You don't have any listings yet.\n\
Use /newlisting to create your first listing!",
Some(keyboard),
)
.await?;
return Ok(());
}
// Create keyboard with buttons for each listing
let mut keyboard = teloxide::types::InlineKeyboardMarkup::default();
for listing in &listings {
keyboard = keyboard.append_row(vec![InlineKeyboardButton::callback(
listing.base.title.to_string(),
listing.persisted.id.to_string(),
)]);
}
// Add back to menu button
keyboard = keyboard.append_row(vec![MyListingsButtons::BackToMenu.into()]);
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,
) -> 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
if let Ok(button) = MyListingsButtons::try_from(data.as_str()) {
match button {
MyListingsButtons::BackToMenu => {
// Transition back to main menu using the reusable function
enter_main_menu(bot, dialogue, target).await?;
return Ok(());
}
}
}
// Otherwise, treat it as a listing ID
let listing_id = ListingDbId::new(data.parse::<i64>()?);
let (_, listing) =
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
dialogue
.update(MyListingsState::ManagingListing(listing_id))
.await?;
show_listing_details(&bot, listing, 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(ManageListingButtons::to_keyboard()),
)
.await?;
Ok(())
}
async fn handle_managing_listing_callback(
db_pool: SqlitePool,
bot: Bot,
dialogue: RootDialogue,
callback_query: CallbackQuery,
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::Edit => {
let (_, listing) =
get_user_and_listing(&db_pool, &bot, from.id, 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?;
show_listings_for_user(db_pool, dialogue, bot, from.id, target).await?;
}
}
Ok(())
}
async fn get_user_and_listing(
db_pool: &SqlitePool,
bot: &Bot,
user_id: teloxide::types::UserId,
listing_id: ListingDbId,
target: impl Into<MessageTarget>,
) -> HandlerResult<(PersistedUser, PersistedListing)> {
let user = match UserDAO::find_by_telegram_id(db_pool, user_id).await? {
Some(user) => user,
None => {
send_message(
bot,
target,
"❌ You don't have an account. Try creating an auction first.",
None,
)
.await?;
return Err(anyhow::anyhow!("User not found"));
}
};
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((user, listing))
}