- Refactor new_listing from single file to modular structure - Add handler factory pattern for state management - Improve keyboard utilities and validations - Update database models for bid, listing, and user systems - Add new types: listing_duration, user_row_id - Remove deprecated user_id type - Update Docker configuration - Enhance test utilities and message handling
271 lines
7.9 KiB
Rust
271 lines
7.9 KiB
Rust
use crate::{
|
|
case,
|
|
db::{Listing, ListingDAO, ListingId, User, UserDAO},
|
|
keyboard_buttons,
|
|
message_utils::{
|
|
extract_callback_data, pluralize_with_count, send_message, HandleAndId, MessageTarget,
|
|
},
|
|
Command, DialogueRootState, HandlerResult, RootDialogue,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::SqlitePool;
|
|
use teloxide::{
|
|
dispatching::{DpHandlerDescription, UpdateFilterExt},
|
|
prelude::*,
|
|
types::{InlineKeyboardButton, Message, MessageId, ParseMode},
|
|
Bot,
|
|
};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum MyListingsState {
|
|
ViewingListings,
|
|
ManagingListing(ListingId),
|
|
EditingListing(ListingId),
|
|
}
|
|
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"),
|
|
]
|
|
}
|
|
}
|
|
|
|
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(())
|
|
}
|
|
|
|
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.id).await?;
|
|
if listings.is_empty() {
|
|
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!",
|
|
None,
|
|
)
|
|
.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.base.id.to_string(),
|
|
)]);
|
|
}
|
|
|
|
let mut response = format!(
|
|
"📋 <b>My Listings</b>\n\n\
|
|
You have {}.\n\n",
|
|
pluralize_with_count(listings.len(), "listing", "listings")
|
|
);
|
|
|
|
// Add each listing with its ID and title
|
|
for listing in &listings {
|
|
response.push_str(&format!(
|
|
"• <b>ID {}:</b> {}\n",
|
|
listing.base.id, listing.base.title
|
|
));
|
|
}
|
|
|
|
response.push_str("\nTap a listing ID below to view details:");
|
|
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);
|
|
|
|
let listing_id = ListingId::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: Listing,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let response = format!(
|
|
"🔍 <b>Viewing Listing Details</b>\n\n\
|
|
<b>Title:</b> {}\n\
|
|
<b>Description:</b> {}\n\
|
|
<b>ID:</b> {}",
|
|
listing.base.title,
|
|
listing
|
|
.base
|
|
.description
|
|
.as_deref()
|
|
.unwrap_or("No description"),
|
|
listing.base.id
|
|
);
|
|
|
|
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: ListingId,
|
|
) -> HandlerResult {
|
|
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
|
let target = (from.clone(), message_id);
|
|
|
|
let button = ManageListingButtons::try_from(data.as_str())
|
|
.map_err(|_| anyhow::anyhow!("Invalid ManageListingButtons callback data: {}", data))?;
|
|
|
|
match button {
|
|
ManageListingButtons::Edit => {
|
|
let (_, listing) =
|
|
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
|
dialogue
|
|
.update(MyListingsState::EditingListing(listing.base.id))
|
|
.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: ListingId,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult<(User, Listing)> {
|
|
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.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))
|
|
}
|