- Replace individual state structs with unified ListingDraft struct - Add EditingListing state with field selection interface - Implement individual field editing states (Title, Description, Price, Slots, StartTime, Duration) - Add field-specific keyboards with Back buttons and Clear functionality for description - Update all handlers to use ListingDraft instead of separate state structs - Rename Confirming to ViewingDraft for clarity - Add proper validation and error handling for all field edits - Enable seamless navigation between edit screen and confirmation - Maintain all existing functionality while adding edit capabilities
1594 lines
47 KiB
Rust
1594 lines
47 KiB
Rust
use chrono::{Duration, Utc};
|
|
use log::info;
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::SqlitePool;
|
|
use teloxide::{
|
|
dispatching::{dialogue::serializer::Json, DpHandlerDescription},
|
|
prelude::*,
|
|
types::{
|
|
CallbackQuery, CallbackQueryId, ChatId, InlineKeyboardButton, InlineKeyboardMarkup,
|
|
Message, ParseMode,
|
|
},
|
|
Bot,
|
|
};
|
|
|
|
use crate::{
|
|
db::{
|
|
dao::ListingDAO,
|
|
models::new_listing::{NewListing, NewListingBase, NewListingFields},
|
|
types::{money_amount::MoneyAmount, user_id::UserId},
|
|
},
|
|
message_utils::{is_cancel, is_cancel_or_no, UserHandleAndId},
|
|
sqlite_storage::SqliteStorage,
|
|
HandlerResult,
|
|
};
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Default)]
|
|
pub struct ListingDraft {
|
|
pub title: String,
|
|
pub description: Option<String>,
|
|
pub buy_now_price: MoneyAmount,
|
|
pub slots_available: i32,
|
|
pub start_hours: i32,
|
|
pub duration_hours: i32,
|
|
}
|
|
|
|
// Dialogue state for the new listing wizard
|
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
|
pub enum ListingWizardState {
|
|
#[default]
|
|
Start,
|
|
AwaitingTitle(ListingDraft),
|
|
AwaitingDescription(ListingDraft),
|
|
AwaitingPrice(ListingDraft),
|
|
AwaitingSlots(ListingDraft),
|
|
AwaitingStartTime(ListingDraft),
|
|
AwaitingDuration(ListingDraft),
|
|
ViewingDraft(ListingDraft),
|
|
EditingListing(ListingDraft),
|
|
EditingTitle(ListingDraft),
|
|
EditingDescription(ListingDraft),
|
|
EditingPrice(ListingDraft),
|
|
EditingSlots(ListingDraft),
|
|
EditingStartTime(ListingDraft),
|
|
EditingDuration(ListingDraft),
|
|
}
|
|
|
|
// Type alias for the dialogue
|
|
pub type NewListingDialogue = Dialogue<ListingWizardState, SqliteStorage<Json>>;
|
|
|
|
// Create the dialogue handler tree for new listing wizard
|
|
pub fn new_listing_dialogue_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
|
dptree::entry()
|
|
.branch(dptree::case![ListingWizardState::Start].endpoint(start_new_listing))
|
|
.branch(
|
|
dptree::case![ListingWizardState::AwaitingTitle(state)].endpoint(handle_title_input),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::AwaitingDescription(state)]
|
|
.endpoint(handle_description_input),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::AwaitingPrice(state)].endpoint(handle_price_input),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::AwaitingSlots(state)].endpoint(handle_slots_input),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::AwaitingStartTime(state)]
|
|
.endpoint(handle_start_time_input),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::AwaitingDuration(state)]
|
|
.endpoint(handle_duration_input),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::ViewingDraft(state)].endpoint(handle_viewing_draft),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingListing(state)]
|
|
.endpoint(handle_editing_screen),
|
|
)
|
|
.branch(dptree::case![ListingWizardState::EditingTitle(state)].endpoint(handle_edit_title))
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingDescription(state)]
|
|
.endpoint(handle_edit_description),
|
|
)
|
|
.branch(dptree::case![ListingWizardState::EditingPrice(state)].endpoint(handle_edit_price))
|
|
.branch(dptree::case![ListingWizardState::EditingSlots(state)].endpoint(handle_edit_slots))
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingStartTime(state)]
|
|
.endpoint(handle_edit_start_time),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingDuration(state)]
|
|
.endpoint(handle_edit_duration),
|
|
)
|
|
}
|
|
|
|
// Handle the /newlisting command - starts the dialogue
|
|
pub async fn handle_new_listing(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
info!(
|
|
"User {} ({}) started new fixed price listing wizard",
|
|
msg.chat.username().unwrap_or("unknown"),
|
|
msg.chat.id
|
|
);
|
|
|
|
let response = "🛍️ <b>Creating New Fixed Price Listing</b>\n\n\
|
|
Let's create your fixed price listing step by step!\n\n\
|
|
<i>Step 1 of 6: Title</i>\n\
|
|
Please enter a title for your listing (max 100 characters):";
|
|
|
|
bot.send_message(msg.chat.id, response)
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
dialogue
|
|
.update(ListingWizardState::AwaitingTitle(ListingDraft::default()))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
// Handle the Start state (same as handle_new_listing for now)
|
|
pub async fn start_new_listing(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
handle_new_listing(bot, dialogue, msg).await
|
|
}
|
|
|
|
// Individual handler functions for each dialogue state
|
|
|
|
pub async fn handle_title_input(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut draft: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} entered title input",
|
|
UserHandleAndId::from_chat(&msg.chat)
|
|
);
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
if text.is_empty() {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Title cannot be empty. Please enter a title for your listing:",
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
|
|
if text.len() > 100 {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Title is too long (max 100 characters). Please enter a shorter title:",
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
|
|
draft.title = text.to_string();
|
|
dialogue
|
|
.update(ListingWizardState::AwaitingDescription(draft))
|
|
.await?;
|
|
|
|
let response = "✅ Title saved!\n\n\
|
|
<i>Step 2 of 6: Description</i>\n\
|
|
Please enter a description for your listing (optional).";
|
|
|
|
let skip_button = InlineKeyboardMarkup::new([[InlineKeyboardButton::callback("Skip", "skip")]]);
|
|
|
|
bot.send_message(chat_id, response)
|
|
.parse_mode(ParseMode::Html)
|
|
.reply_markup(skip_button)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_description_input(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut draft: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} entered description input",
|
|
UserHandleAndId::from_chat(&msg.chat)
|
|
);
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
if text.len() > 1000 {
|
|
bot.send_message(chat_id, "❌ Description is too long (max 1000 characters). Please enter a shorter description or 'skip':")
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
draft.description = Some(text.to_string());
|
|
|
|
dialogue
|
|
.update(ListingWizardState::AwaitingPrice(draft))
|
|
.await?;
|
|
|
|
let response = "✅ Description saved!\n\n\
|
|
<i>Step 3 of 6: Price</i>\n\
|
|
Please enter the fixed price for your listing (e.g., 10.50, 25, 0.99):\n\n\
|
|
💡 <i>Price should be in USD</i>";
|
|
|
|
bot.send_message(chat_id, response)
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_description_callback(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
draft: ListingDraft,
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let data = match callback_query.data.as_deref() {
|
|
Some(data) => data,
|
|
None => return Ok(()),
|
|
};
|
|
|
|
if data == "skip" {
|
|
// Answer the callback query to remove the loading state
|
|
bot.answer_callback_query(callback_query.id).await?;
|
|
|
|
// Process as if user typed "skip"
|
|
dialogue
|
|
.update(ListingWizardState::AwaitingPrice(draft))
|
|
.await?;
|
|
|
|
let response = "✅ Description skipped!\n\n\
|
|
<i>Step 3 of 6: Price</i>\n\
|
|
Please enter the fixed price for your listing (e.g., 10.50, 25, 0.99):\n\n\
|
|
💡 <i>Price should be in USD</i>";
|
|
|
|
if let Some(message) = callback_query.message {
|
|
let chat_id = message.chat().id;
|
|
bot.edit_message_text(chat_id, message.id(), response)
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Create callback query handler for skip button, slots buttons, and start time button
|
|
pub fn new_listing_callback_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
|
dptree::entry()
|
|
.branch(
|
|
dptree::case![ListingWizardState::AwaitingDescription(state)]
|
|
.endpoint(handle_description_callback),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::AwaitingSlots(state)].endpoint(handle_slots_callback),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::AwaitingStartTime(state)]
|
|
.endpoint(handle_start_time_callback),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::ViewingDraft(state)]
|
|
.endpoint(handle_viewing_draft_callback),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingListing(state)]
|
|
.endpoint(handle_editing_callback),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingTitle(state)]
|
|
.endpoint(handle_edit_field_callback),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingDescription(state)]
|
|
.endpoint(handle_edit_field_callback),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingPrice(state)]
|
|
.endpoint(handle_edit_field_callback),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingSlots(state)]
|
|
.endpoint(handle_edit_field_callback),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingStartTime(state)]
|
|
.endpoint(handle_edit_field_callback),
|
|
)
|
|
.branch(
|
|
dptree::case![ListingWizardState::EditingDuration(state)]
|
|
.endpoint(handle_edit_field_callback),
|
|
)
|
|
}
|
|
|
|
pub async fn handle_slots_callback(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
draft: ListingDraft,
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let data = match callback_query.data.as_deref() {
|
|
Some(data) => data,
|
|
None => return Ok(()),
|
|
};
|
|
|
|
if data.starts_with("slots_") {
|
|
info!(
|
|
"User {} selected slots button: {}",
|
|
UserHandleAndId::from_user(&callback_query.from),
|
|
data
|
|
);
|
|
|
|
// Extract the slots number from the callback data
|
|
let slots_str = data.strip_prefix("slots_").unwrap();
|
|
if let Ok(slots) = slots_str.parse::<i32>() {
|
|
if let Some(message) = callback_query.message {
|
|
// Answer the callback query to remove the loading state
|
|
bot.answer_callback_query(callback_query.id).await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
// Process the slots selection and send response using shared logic
|
|
process_slots_and_respond(&bot, dialogue, draft, chat_id, slots).await?;
|
|
} else {
|
|
handle_callback_error(&bot, dialogue, callback_query.id).await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_start_time_callback(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
draft: ListingDraft,
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let data = match callback_query.data.as_deref() {
|
|
Some(data) => data,
|
|
None => return Ok(()),
|
|
};
|
|
|
|
if data == "start_time_now" {
|
|
info!(
|
|
"User {} selected 'Now' for start time",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
if let Some(message) = callback_query.message {
|
|
// Answer the callback query to remove the loading state
|
|
bot.answer_callback_query(callback_query.id).await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
// Process start time of 0 (immediate start)
|
|
process_start_time_and_respond(&bot, dialogue, draft, chat_id, 0).await?;
|
|
} else {
|
|
handle_callback_error(&bot, dialogue, callback_query.id).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Helper function to handle callback query errors (missing message)
|
|
async fn handle_callback_error(
|
|
bot: &Bot,
|
|
dialogue: NewListingDialogue,
|
|
callback_query_id: CallbackQueryId,
|
|
) -> HandlerResult {
|
|
// Reset dialogue to idle state
|
|
dialogue.exit().await?;
|
|
|
|
// Answer callback query with error message
|
|
bot.answer_callback_query(callback_query_id)
|
|
.text("❌ Error: Unable to process request. Please try again.")
|
|
.show_alert(true)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Helper function to process slots input, update dialogue state, and send response
|
|
async fn process_slots_and_respond(
|
|
bot: &Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut draft: ListingDraft,
|
|
chat_id: ChatId,
|
|
slots: i32,
|
|
) -> HandlerResult {
|
|
// Update dialogue state
|
|
draft.slots_available = slots;
|
|
dialogue
|
|
.update(ListingWizardState::AwaitingStartTime(draft))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Send response message with inline button
|
|
let response = format!(
|
|
"✅ Available slots: <b>{slots}</b>\n\n\
|
|
<i>Step 5 of 6: Start Time</i>\n\
|
|
When should your listing start?\n\
|
|
• Click 'Now' to start immediately\n\
|
|
• Enter number of hours to delay (e.g., '2' for 2 hours from now)\n\
|
|
• Maximum delay: 168 hours (7 days)"
|
|
);
|
|
|
|
bot.send_message(chat_id, response)
|
|
.parse_mode(ParseMode::Html)
|
|
.reply_markup(create_start_time_keyboard())
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_viewing_draft_callback(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
state: ListingDraft,
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let data = match callback_query.data.as_deref() {
|
|
Some(data) => data,
|
|
None => return Ok(()),
|
|
};
|
|
|
|
let callback_id = callback_query.id;
|
|
|
|
// Answer the callback query to remove the loading state
|
|
bot.answer_callback_query(callback_id.clone()).await?;
|
|
let message = match callback_query.message {
|
|
Some(message) => message,
|
|
None => {
|
|
handle_callback_error(&bot, dialogue, callback_id).await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
match data {
|
|
"confirm_create" => {
|
|
info!(
|
|
"User {} confirmed listing creation",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
// Exit dialogue and create listing
|
|
dialogue.exit().await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
let response = "✅ <b>Listing Created Successfully!</b>\n\n\
|
|
Your fixed price listing has been created and is now active.\n\
|
|
Other users can now purchase items from your listing.";
|
|
|
|
bot.edit_message_text(chat_id, message.id(), response)
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
}
|
|
"confirm_discard" => {
|
|
info!(
|
|
"User {} discarded listing creation",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
// Exit dialogue and send cancellation message
|
|
dialogue.exit().await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
let response = "🗑️ <b>Listing Discarded</b>\n\n\
|
|
Your listing has been discarded and not created.\n\
|
|
You can start a new listing anytime with /newlisting.";
|
|
|
|
bot.edit_message_text(chat_id, message.id(), response)
|
|
.await?;
|
|
}
|
|
"confirm_edit" => {
|
|
info!(
|
|
"User {} chose to edit listing",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
// Go to editing state to allow user to modify specific fields
|
|
dialogue
|
|
.update(ListingWizardState::EditingListing(state.clone()))
|
|
.await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
|
|
// Delete the old message and show the edit screen
|
|
bot.delete_message(chat_id, message.id()).await?;
|
|
show_edit_screen(bot, chat_id, state).await?;
|
|
}
|
|
_ => {
|
|
// Unknown callback data, ignore
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Helper function to create start time inline keyboard with "Now" button
|
|
fn create_start_time_keyboard() -> InlineKeyboardMarkup {
|
|
InlineKeyboardMarkup::new([[InlineKeyboardButton::callback("Now", "start_time_now")]])
|
|
}
|
|
|
|
// Helper function to create confirmation inline keyboard with Create/Discard/Edit buttons
|
|
fn create_confirmation_keyboard() -> InlineKeyboardMarkup {
|
|
InlineKeyboardMarkup::default()
|
|
.append_row([
|
|
InlineKeyboardButton::callback("✅ Create", "confirm_create"),
|
|
InlineKeyboardButton::callback("🗑️ Discard", "confirm_discard"),
|
|
])
|
|
.append_row([InlineKeyboardButton::callback("✏️ Edit", "confirm_edit")])
|
|
}
|
|
|
|
// Helper function to create field selection keyboard for editing
|
|
fn create_field_selection_keyboard() -> InlineKeyboardMarkup {
|
|
InlineKeyboardMarkup::default()
|
|
.append_row([
|
|
InlineKeyboardButton::callback("📝 Title", "edit_title"),
|
|
InlineKeyboardButton::callback("📄 Description", "edit_description"),
|
|
])
|
|
.append_row([
|
|
InlineKeyboardButton::callback("💰 Price", "edit_price"),
|
|
InlineKeyboardButton::callback("🔢 Slots", "edit_slots"),
|
|
])
|
|
.append_row([
|
|
InlineKeyboardButton::callback("⏰ Start Time", "edit_start_time"),
|
|
InlineKeyboardButton::callback("⏳ Duration", "edit_duration"),
|
|
])
|
|
.append_row([InlineKeyboardButton::callback(
|
|
"✅ Done Editing",
|
|
"edit_done",
|
|
)])
|
|
}
|
|
|
|
// Helper function to create back button keyboard
|
|
fn create_back_button_keyboard() -> InlineKeyboardMarkup {
|
|
InlineKeyboardMarkup::new([[InlineKeyboardButton::callback("🔙 Back", "edit_back")]])
|
|
}
|
|
|
|
fn create_back_button_keyboard_with_clear(field: &str) -> InlineKeyboardMarkup {
|
|
InlineKeyboardMarkup::new([[
|
|
InlineKeyboardButton::callback("🔙 Back", "edit_back"),
|
|
InlineKeyboardButton::callback(
|
|
format!("🧹 Clear {}", field),
|
|
format!("edit_clear_{}", field),
|
|
),
|
|
]])
|
|
}
|
|
|
|
// Helper function to process start time input, update dialogue state, and send response
|
|
async fn process_start_time_and_respond(
|
|
bot: &Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut draft: ListingDraft,
|
|
chat_id: ChatId,
|
|
hours: i32,
|
|
) -> HandlerResult {
|
|
// Update dialogue state
|
|
draft.start_hours = hours;
|
|
dialogue
|
|
.update(ListingWizardState::AwaitingDuration(draft))
|
|
.await?;
|
|
|
|
// Generate response message
|
|
let start_msg = if hours == 0 {
|
|
"immediately".to_string()
|
|
} else {
|
|
format!("in {} hour{}", hours, if hours == 1 { "" } else { "s" })
|
|
};
|
|
|
|
let response = format!(
|
|
"✅ Listing will start: <b>{}</b>\n\n\
|
|
<i>Step 6 of 6: Duration</i>\n\
|
|
How long should your listing run?\n\
|
|
Enter duration in hours (minimum 1 hour, maximum 720 hours / 30 days):",
|
|
start_msg
|
|
);
|
|
|
|
bot.send_message(chat_id, response)
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_price_input(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut draft: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} entered price input",
|
|
UserHandleAndId::from_chat(&msg.chat)
|
|
);
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
let price = match MoneyAmount::from_str(text) {
|
|
Ok(amount) => {
|
|
if amount.cents() <= 0 {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Price must be greater than $0.00. Please enter a valid price:",
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
amount
|
|
}
|
|
Err(_) => {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Invalid price format. Please enter a valid price (e.g., 10.50, 25, 0.99):",
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
draft.buy_now_price = price;
|
|
dialogue
|
|
.update(ListingWizardState::AwaitingSlots(draft))
|
|
.await?;
|
|
|
|
let response = format!(
|
|
"✅ Price saved: <b>${}</b>\n\n\
|
|
<i>Step 4 of 6: Available Slots</i>\n\
|
|
How many items are available for sale?\n\n\
|
|
Choose a common value below or enter a custom number (1-1000):",
|
|
price
|
|
);
|
|
|
|
let slots_buttons = InlineKeyboardMarkup::new([
|
|
[
|
|
InlineKeyboardButton::callback("1", "slots_1"),
|
|
InlineKeyboardButton::callback("2", "slots_2"),
|
|
],
|
|
[
|
|
InlineKeyboardButton::callback("5", "slots_5"),
|
|
InlineKeyboardButton::callback("10", "slots_10"),
|
|
],
|
|
]);
|
|
|
|
bot.send_message(chat_id, response)
|
|
.parse_mode(ParseMode::Html)
|
|
.reply_markup(slots_buttons)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_slots_input(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
draft: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} entered slots input",
|
|
UserHandleAndId::from_chat(&msg.chat)
|
|
);
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
let slots = match text.parse::<i32>() {
|
|
Ok(num) => {
|
|
if num < 1 || num > 1000 {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Number of slots must be between 1 and 1000. Please enter a valid number:",
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
num
|
|
}
|
|
Err(_) => {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Invalid number. Please enter a number from 1 to 1000:",
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
process_slots_and_respond(&bot, dialogue, draft, chat_id, slots).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_start_time_input(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
draft: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} entered start time input",
|
|
UserHandleAndId::from_chat(&msg.chat)
|
|
);
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
let hours = match text.parse::<i32>() {
|
|
Ok(num) => {
|
|
if num < 0 || num > 168 {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Start time must be between 0 and 168 hours. Please enter a valid number:",
|
|
)
|
|
.reply_markup(create_start_time_keyboard())
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
num
|
|
}
|
|
Err(_) => {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Invalid number. Please enter hours (0 for immediate, up to 168 for 7 days):",
|
|
)
|
|
.reply_markup(create_start_time_keyboard())
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
process_start_time_and_respond(&bot, dialogue, draft, chat_id, hours).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_duration_input(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut draft: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} entered duration input",
|
|
UserHandleAndId::from_chat(&msg.chat)
|
|
);
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
let duration = match text.parse::<i32>() {
|
|
Ok(num) => {
|
|
if num < 1 || num > 720 {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Duration must be between 1 and 720 hours. Please enter a valid number:",
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
num
|
|
}
|
|
Err(_) => {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Invalid number. Please enter duration in hours (1-720):",
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
draft.duration_hours = duration;
|
|
dialogue
|
|
.update(ListingWizardState::ViewingDraft(draft.clone()))
|
|
.await?;
|
|
|
|
show_confirmation(bot, chat_id, draft).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn show_confirmation(bot: Bot, chat_id: ChatId, state: ListingDraft) -> HandlerResult {
|
|
let description_text = state.description.as_deref().unwrap_or("*No description*");
|
|
|
|
let start_time_str = if state.start_hours == 0 {
|
|
"Immediately".to_string()
|
|
} else {
|
|
format!(
|
|
"In {} hour{}",
|
|
state.start_hours,
|
|
if state.start_hours == 1 { "" } else { "s" }
|
|
)
|
|
};
|
|
|
|
let response = format!(
|
|
"📋 <b>Listing Summary</b>\n\n\
|
|
<b>Title:</b> {}\n\
|
|
<b>Description:</b> {}\n\
|
|
<b>Price:</b> ${}\n\
|
|
<b>Available Slots:</b> {}\n\
|
|
<b>Start Time:</b> {}\n\
|
|
<b>Duration:</b> {} hour{}\n\n\
|
|
Please review your listing and choose an action:",
|
|
state.title,
|
|
description_text,
|
|
state.buy_now_price,
|
|
state.slots_available,
|
|
start_time_str,
|
|
state.duration_hours,
|
|
if state.duration_hours == 1 { "" } else { "s" }
|
|
);
|
|
|
|
bot.send_message(chat_id, response)
|
|
.parse_mode(ParseMode::Html)
|
|
.reply_markup(create_confirmation_keyboard())
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn show_edit_screen(bot: Bot, chat_id: ChatId, state: ListingDraft) -> HandlerResult {
|
|
let description_text = state.description.as_deref().unwrap_or("*No description*");
|
|
|
|
let start_time_str = if state.start_hours == 0 {
|
|
"Immediately".to_string()
|
|
} else {
|
|
format!(
|
|
"In {} hour{}",
|
|
state.start_hours,
|
|
if state.start_hours == 1 { "" } else { "s" }
|
|
)
|
|
};
|
|
|
|
let response = format!(
|
|
"✏️ <b>Edit Listing</b>\n\n\
|
|
<b>Current Values:</b>\n\
|
|
📝 <b>Title:</b> {}\n\
|
|
📄 <b>Description:</b> {}\n\
|
|
💰 <b>Price:</b> ${}\n\
|
|
🔢 <b>Available Slots:</b> {}\n\
|
|
⏰ <b>Start Time:</b> {}\n\
|
|
⏳ <b>Duration:</b> {} hour{}\n\n\
|
|
Select a field to edit:",
|
|
state.title,
|
|
description_text,
|
|
state.buy_now_price,
|
|
state.slots_available,
|
|
start_time_str,
|
|
state.duration_hours,
|
|
if state.duration_hours == 1 { "" } else { "s" }
|
|
);
|
|
|
|
bot.send_message(chat_id, response)
|
|
.parse_mode(ParseMode::Html)
|
|
.reply_markup(create_field_selection_keyboard())
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_viewing_draft(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
state: ListingDraft,
|
|
msg: Message,
|
|
db_pool: SqlitePool,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} entered confirmation input",
|
|
UserHandleAndId::from_chat(&msg.chat)
|
|
);
|
|
|
|
if is_cancel_or_no(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
if text.eq_ignore_ascii_case("yes") {
|
|
create_listing(
|
|
bot,
|
|
dialogue,
|
|
msg,
|
|
db_pool,
|
|
state.title,
|
|
state.description,
|
|
state.buy_now_price,
|
|
state.slots_available,
|
|
state.start_hours,
|
|
state.duration_hours,
|
|
)
|
|
.await?;
|
|
} else {
|
|
bot.send_message(chat_id, "Please confirm your choice:")
|
|
.reply_markup(create_confirmation_keyboard())
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_editing_screen(bot: Bot, state: ListingDraft, msg: Message) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
|
|
info!(
|
|
"User {} in editing screen, showing field selection",
|
|
UserHandleAndId::from_chat(&msg.chat)
|
|
);
|
|
|
|
// Show the edit screen with current values and field selection
|
|
show_edit_screen(bot, chat_id, state).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn create_listing(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
msg: Message,
|
|
db_pool: SqlitePool,
|
|
title: String,
|
|
description: Option<String>,
|
|
buy_now_price: MoneyAmount,
|
|
slots_available: i32,
|
|
start_hours: i32,
|
|
duration_hours: i32,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let user_id = msg
|
|
.from
|
|
.as_ref()
|
|
.map(|u| u.id.0 as i64)
|
|
.unwrap_or(chat_id.0);
|
|
|
|
let now = Utc::now();
|
|
let starts_at = now + Duration::hours(start_hours as i64);
|
|
let ends_at = starts_at + Duration::hours(duration_hours as i64);
|
|
|
|
let new_listing_base = NewListingBase::new(
|
|
UserId::new(user_id),
|
|
title.clone(),
|
|
description,
|
|
starts_at,
|
|
ends_at,
|
|
);
|
|
|
|
let new_listing = NewListing {
|
|
base: new_listing_base,
|
|
fields: NewListingFields::FixedPriceListing {
|
|
buy_now_price,
|
|
slots_available,
|
|
},
|
|
};
|
|
|
|
match ListingDAO::insert_listing(&db_pool, &new_listing).await {
|
|
Ok(listing) => {
|
|
let response = format!(
|
|
"✅ <b>Listing Created Successfully!</b>\n\n\
|
|
<b>Listing ID:</b> {}\n\
|
|
<b>Title:</b> {}\n\
|
|
<b>Price:</b> ${}\n\
|
|
<b>Slots Available:</b> {}\n\n\
|
|
Your fixed price listing is now live! 🎉",
|
|
listing.base.id, listing.base.title, buy_now_price, slots_available
|
|
);
|
|
|
|
bot.send_message(chat_id, response)
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
dialogue.exit().await?;
|
|
|
|
info!(
|
|
"Fixed price listing created successfully for user {}: {:?}",
|
|
user_id, listing.base.id
|
|
);
|
|
}
|
|
Err(e) => {
|
|
log::error!("Failed to create listing for user {}: {}", user_id, e);
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ **Error:** Failed to create listing. Please try again later.",
|
|
)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn cancel_wizard(bot: Bot, dialogue: NewListingDialogue, msg: Message) -> HandlerResult {
|
|
info!(
|
|
"User {} cancelled new listing wizard",
|
|
UserHandleAndId::from_chat(&msg.chat)
|
|
);
|
|
dialogue.exit().await?;
|
|
bot.send_message(msg.chat.id, "❌ Listing creation cancelled.")
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Individual field editing handlers
|
|
pub async fn handle_edit_title(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut state: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} editing title: '{}'",
|
|
UserHandleAndId::from_chat(&msg.chat),
|
|
text
|
|
);
|
|
|
|
if text.is_empty() {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Title cannot be empty. Please enter a valid title:",
|
|
)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
|
|
if text.len() > 100 {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Title too long. Please enter a title with 100 characters or less:",
|
|
)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
|
|
// Update the title
|
|
state.title = text.to_string();
|
|
|
|
// Go back to editing listing state
|
|
dialogue
|
|
.update(ListingWizardState::EditingListing(state.clone()))
|
|
.await?;
|
|
|
|
bot.send_message(chat_id, "✅ Title updated!").await?;
|
|
|
|
show_edit_screen(bot, chat_id, state).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_edit_description(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut state: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} editing description: '{}'",
|
|
UserHandleAndId::from_chat(&msg.chat),
|
|
text
|
|
);
|
|
|
|
if text.eq_ignore_ascii_case("none") {
|
|
state.description = None;
|
|
} else if text.is_empty() {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Please enter a description or type 'none' for no description:",
|
|
)
|
|
.reply_markup(create_back_button_keyboard_with_clear("description"))
|
|
.await?;
|
|
return Ok(());
|
|
} else if text.len() > 500 {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Description too long. Please enter a description with 500 characters or less:",
|
|
)
|
|
.reply_markup(create_back_button_keyboard_with_clear("description"))
|
|
.await?;
|
|
return Ok(());
|
|
} else {
|
|
state.description = Some(text.to_string());
|
|
}
|
|
|
|
// Go back to editing listing state
|
|
dialogue
|
|
.update(ListingWizardState::EditingListing(state.clone()))
|
|
.await?;
|
|
|
|
bot.send_message(chat_id, "✅ Description updated!").await?;
|
|
|
|
show_edit_screen(bot, chat_id, state).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_edit_price(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut state: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} editing price: '{}'",
|
|
UserHandleAndId::from_chat(&msg.chat),
|
|
text
|
|
);
|
|
|
|
let price = match text.parse::<f64>() {
|
|
Ok(p) if p > 0.0 && p <= 10000.0 => match MoneyAmount::from_str(&format!("{:.2}", p)) {
|
|
Ok(amount) => amount,
|
|
Err(_) => {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Invalid price format. Please enter a price between $0.01 and $10,000.00 (e.g., 25.99):",
|
|
)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
},
|
|
_ => {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Invalid price. Please enter a price between $0.01 and $10,000.00 (e.g., 25.99):",
|
|
)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// Update the price
|
|
state.buy_now_price = price;
|
|
|
|
// Go back to editing listing state
|
|
dialogue
|
|
.update(ListingWizardState::EditingListing(state.clone()))
|
|
.await?;
|
|
|
|
bot.send_message(chat_id, "✅ Price updated!").await?;
|
|
|
|
show_edit_screen(bot, chat_id, state).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_edit_slots(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut state: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} editing slots: '{}'",
|
|
UserHandleAndId::from_chat(&msg.chat),
|
|
text
|
|
);
|
|
|
|
let slots = match text.parse::<i32>() {
|
|
Ok(s) if (1..=1000).contains(&s) => s,
|
|
_ => {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Invalid number. Please enter a number between 1 and 1000:",
|
|
)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// Update the slots
|
|
state.slots_available = slots;
|
|
|
|
// Go back to editing listing state
|
|
dialogue
|
|
.update(ListingWizardState::EditingListing(state.clone()))
|
|
.await?;
|
|
|
|
bot.send_message(chat_id, "✅ Slots updated!").await?;
|
|
|
|
show_edit_screen(bot, chat_id, state).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_edit_start_time(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut state: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} editing start time: '{}'",
|
|
UserHandleAndId::from_chat(&msg.chat),
|
|
text
|
|
);
|
|
|
|
let start_hours = match text.parse::<i32>() {
|
|
Ok(h) if (0..=168).contains(&h) => h,
|
|
_ => {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Invalid number. Please enter hours from now (0-168):",
|
|
)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// Update the start time
|
|
state.start_hours = start_hours;
|
|
|
|
// Go back to editing listing state
|
|
dialogue
|
|
.update(ListingWizardState::EditingListing(state.clone()))
|
|
.await?;
|
|
|
|
bot.send_message(chat_id, "✅ Start time updated!").await?;
|
|
|
|
show_edit_screen(bot, chat_id, state).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_edit_duration(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut state: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!(
|
|
"User {} editing duration: '{}'",
|
|
UserHandleAndId::from_chat(&msg.chat),
|
|
text
|
|
);
|
|
|
|
let duration = match text.parse::<i32>() {
|
|
Ok(d) if (1..=720).contains(&d) => d,
|
|
_ => {
|
|
bot.send_message(
|
|
chat_id,
|
|
"❌ Invalid number. Please enter duration in hours (1-720):",
|
|
)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// Update the duration
|
|
state.duration_hours = duration;
|
|
|
|
// Go back to editing listing state
|
|
dialogue
|
|
.update(ListingWizardState::EditingListing(state.clone()))
|
|
.await?;
|
|
|
|
bot.send_message(chat_id, "✅ Duration updated!").await?;
|
|
|
|
show_edit_screen(bot, chat_id, state).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_editing_callback(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
state: ListingDraft,
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let data = match callback_query.data.as_deref() {
|
|
Some(data) => data,
|
|
None => return Ok(()),
|
|
};
|
|
|
|
let callback_id = callback_query.id.clone();
|
|
|
|
// Answer the callback query to remove the loading state
|
|
bot.answer_callback_query(callback_id.clone()).await?;
|
|
let message = match callback_query.message {
|
|
Some(message) => message,
|
|
None => {
|
|
handle_callback_error(&bot, dialogue, callback_id).await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
match data {
|
|
"edit_title" => {
|
|
info!(
|
|
"User {} chose to edit title",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
dialogue
|
|
.update(ListingWizardState::EditingTitle(state.clone()))
|
|
.await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
let response = format!(
|
|
"📝 <b>Edit Title</b>\n\nCurrent title: <i>{}</i>\n\nPlease enter the new title for your listing:",
|
|
state.title
|
|
);
|
|
|
|
bot.edit_message_text(chat_id, message.id(), response)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.await?;
|
|
}
|
|
"edit_description" => {
|
|
info!(
|
|
"User {} chose to edit description",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
dialogue
|
|
.update(ListingWizardState::EditingDescription(state.clone()))
|
|
.await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
let current_desc = state.description.as_deref().unwrap_or("*No description*");
|
|
let response = format!(
|
|
"📄 <b>Edit Description</b>\n\nCurrent description: <i>{}</i>\n\nPlease enter the new description for your listing (or type 'none' for no description):",
|
|
current_desc
|
|
);
|
|
|
|
bot.edit_message_text(chat_id, message.id(), response)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
}
|
|
"edit_clear_description" => {
|
|
info!(
|
|
"User {} chose to clear description",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
// clear the description and go back to the edit screen
|
|
let mut state = state.clone();
|
|
state.description = None;
|
|
dialogue
|
|
.update(ListingWizardState::EditingListing(state.clone()))
|
|
.await?;
|
|
show_edit_screen(bot, message.chat().id, state).await?;
|
|
}
|
|
"edit_price" => {
|
|
info!(
|
|
"User {} chose to edit price",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
dialogue
|
|
.update(ListingWizardState::EditingPrice(state.clone()))
|
|
.await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
let response = format!(
|
|
"💰 <b>Edit Price</b>\n\nCurrent price: <i>${}</i>\n\nPlease enter the new buy-now price in USD (e.g., 25.99):",
|
|
state.buy_now_price
|
|
);
|
|
|
|
bot.edit_message_text(chat_id, message.id(), response)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
}
|
|
"edit_slots" => {
|
|
info!(
|
|
"User {} chose to edit slots",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
dialogue
|
|
.update(ListingWizardState::EditingSlots(state.clone()))
|
|
.await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
let response = format!(
|
|
"🔢 <b>Edit Slots</b>\n\nCurrent slots: <i>{}</i>\n\nPlease enter the number of available slots (1-1000):",
|
|
state.slots_available
|
|
);
|
|
|
|
bot.edit_message_text(chat_id, message.id(), response)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
}
|
|
"edit_start_time" => {
|
|
info!(
|
|
"User {} chose to edit start time",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
dialogue
|
|
.update(ListingWizardState::EditingStartTime(state.clone()))
|
|
.await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
let current_start = if state.start_hours == 0 {
|
|
"Immediately".to_string()
|
|
} else {
|
|
format!(
|
|
"In {} hour{}",
|
|
state.start_hours,
|
|
if state.start_hours == 1 { "" } else { "s" }
|
|
)
|
|
};
|
|
|
|
let response = format!(
|
|
"⏰ <b>Edit Start Time</b>\n\nCurrent start time: <i>{}</i>\n\nPlease enter how many hours from now the listing should start (0 for immediate start, 1-168 for delayed start):",
|
|
current_start
|
|
);
|
|
|
|
bot.edit_message_text(chat_id, message.id(), response)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
}
|
|
"edit_duration" => {
|
|
info!(
|
|
"User {} chose to edit duration",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
dialogue
|
|
.update(ListingWizardState::EditingDuration(state.clone()))
|
|
.await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
let response = format!(
|
|
"⏳ <b>Edit Duration</b>\n\nCurrent duration: <i>{} hour{}</i>\n\nPlease enter the listing duration in hours (1-720):",
|
|
state.duration_hours,
|
|
if state.duration_hours == 1 { "" } else { "s" }
|
|
);
|
|
|
|
bot.edit_message_text(chat_id, message.id(), response)
|
|
.reply_markup(create_back_button_keyboard())
|
|
.parse_mode(ParseMode::Html)
|
|
.await?;
|
|
}
|
|
"edit_back" => {
|
|
info!(
|
|
"User {} chose to go back to edit screen",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
let chat_id = message.chat().id;
|
|
|
|
// Delete the current message and show the edit screen again
|
|
bot.delete_message(chat_id, message.id()).await?;
|
|
show_edit_screen(bot, chat_id, state).await?;
|
|
}
|
|
"edit_done" => {
|
|
info!(
|
|
"User {} finished editing, going back to confirmation",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
// Go back to confirmation state
|
|
dialogue
|
|
.update(ListingWizardState::ViewingDraft(state.clone()))
|
|
.await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
|
|
// Delete the edit screen and show confirmation
|
|
bot.delete_message(chat_id, message.id()).await?;
|
|
show_confirmation(bot, chat_id, state).await?;
|
|
}
|
|
_ => {
|
|
// Unknown callback data, ignore
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_edit_field_callback(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
state: ListingDraft,
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let data = match callback_query.data.as_deref() {
|
|
Some(data) => data,
|
|
None => return Ok(()),
|
|
};
|
|
|
|
let callback_id = callback_query.id.clone();
|
|
|
|
// Answer the callback query to remove the loading state
|
|
bot.answer_callback_query(callback_id.clone()).await?;
|
|
let message = match callback_query.message {
|
|
Some(message) => message,
|
|
None => {
|
|
handle_callback_error(&bot, dialogue, callback_id).await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
match data {
|
|
"edit_back" => {
|
|
info!(
|
|
"User {} chose to go back from field editing",
|
|
UserHandleAndId::from_user(&callback_query.from)
|
|
);
|
|
|
|
// Go back to editing listing state
|
|
dialogue
|
|
.update(ListingWizardState::EditingListing(state.clone()))
|
|
.await?;
|
|
|
|
let chat_id = message.chat().id;
|
|
|
|
// Delete the current message and show the edit screen again
|
|
bot.delete_message(chat_id, message.id()).await?;
|
|
show_edit_screen(bot, chat_id, state).await?;
|
|
}
|
|
_ => {
|
|
// Unknown callback data, ignore
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|