- Created edit_html_message utility for consistent message editing - Converted all bot.edit_message_text() calls to use edit_html_message - Fixed markdown to HTML conversion (**Error:** → <b>Error:</b>, *text* → <i>text</i>) - Added MessageId import to fix compilation errors - 100% message utility adoption achieved - all direct bot message calls eliminated
1759 lines
53 KiB
Rust
1759 lines
53 KiB
Rust
use crate::{
|
|
db::{
|
|
dao::ListingDAO,
|
|
models::new_listing::{NewListing, NewListingBase, NewListingFields},
|
|
types::{money_amount::MoneyAmount, user_id::UserId},
|
|
},
|
|
message_utils::{
|
|
edit_html_message, is_cancel, is_cancel_or_no, send_html_message, UserHandleAndId,
|
|
},
|
|
sqlite_storage::SqliteStorage,
|
|
Command, HandlerResult,
|
|
};
|
|
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,
|
|
},
|
|
Bot,
|
|
};
|
|
|
|
#[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
|
|
type NewListingDialogue = Dialogue<ListingWizardState, SqliteStorage<Json>>;
|
|
|
|
// ============================================================================
|
|
// UTILITY FUNCTIONS FOR CODE DEDUPLICATION
|
|
// ============================================================================
|
|
|
|
// Extract callback data and answer callback query
|
|
async fn extract_callback_data(
|
|
bot: &Bot,
|
|
callback_query: &CallbackQuery,
|
|
) -> Result<(String, ChatId), HandlerResult> {
|
|
let data = match callback_query.data.as_deref() {
|
|
Some(data) => data.to_string(),
|
|
None => return Err(Ok(())), // Early return for missing data
|
|
};
|
|
|
|
let chat_id = match &callback_query.message {
|
|
Some(message) => message.chat().id,
|
|
None => return Err(Ok(())), // Early return for missing message
|
|
};
|
|
|
|
// Answer the callback query to remove loading state
|
|
if let Err(e) = bot.answer_callback_query(callback_query.id.clone()).await {
|
|
log::warn!("Failed to answer callback query: {}", e);
|
|
}
|
|
|
|
Ok((data, chat_id))
|
|
}
|
|
|
|
// Common input validation functions
|
|
fn validate_title(text: &str) -> Result<String, String> {
|
|
if text.is_empty() {
|
|
return Err("❌ Title cannot be empty. Please enter a title for your listing:".to_string());
|
|
}
|
|
if text.len() > 100 {
|
|
return Err(
|
|
"❌ Title is too long (max 100 characters). Please enter a shorter title:".to_string(),
|
|
);
|
|
}
|
|
Ok(text.to_string())
|
|
}
|
|
|
|
fn validate_price(text: &str) -> Result<MoneyAmount, String> {
|
|
match MoneyAmount::from_str(text) {
|
|
Ok(amount) => {
|
|
if amount.cents() <= 0 {
|
|
Err("❌ Price must be greater than $0.00. Please enter a valid price:".to_string())
|
|
} else {
|
|
Ok(amount)
|
|
}
|
|
}
|
|
Err(_) => Err(
|
|
"❌ Invalid price format. Please enter a valid price (e.g., 10.50, 25, 0.99):"
|
|
.to_string(),
|
|
),
|
|
}
|
|
}
|
|
|
|
fn validate_slots(text: &str) -> Result<i32, String> {
|
|
match text.parse::<i32>() {
|
|
Ok(slots) if slots >= 1 && slots <= 1000 => Ok(slots),
|
|
Ok(_) => Err(
|
|
"❌ Number of slots must be between 1 and 1000. Please enter a valid number:"
|
|
.to_string(),
|
|
),
|
|
Err(_) => Err("❌ Invalid number. Please enter a number from 1 to 1000:".to_string()),
|
|
}
|
|
}
|
|
|
|
fn validate_duration(text: &str) -> Result<i32, String> {
|
|
match text.parse::<i32>() {
|
|
Ok(hours) if hours >= 1 && hours <= 720 => Ok(hours), // 1 hour to 30 days
|
|
Ok(_) => Err(
|
|
"❌ Duration must be between 1 and 720 hours. Please enter a valid number:".to_string(),
|
|
),
|
|
Err(_) => Err("❌ Invalid number. Please enter number of hours (1-720):".to_string()),
|
|
}
|
|
}
|
|
|
|
fn validate_start_time(text: &str) -> Result<i32, String> {
|
|
match text.parse::<i32>() {
|
|
Ok(hours) if hours >= 0 && hours <= 168 => Ok(hours), // Max 1 week delay
|
|
Ok(_) => Err(
|
|
"❌ Start time must be between 0 and 168 hours. Please enter a valid number:"
|
|
.to_string(),
|
|
),
|
|
Err(_) => Err(
|
|
"❌ Invalid number. Please enter number of hours (0 for immediate start):".to_string(),
|
|
),
|
|
}
|
|
}
|
|
|
|
// Logging utility for user actions
|
|
fn log_user_action(msg: &Message, action: &str) {
|
|
info!("User {} {}", UserHandleAndId::from_chat(&msg.chat), action);
|
|
}
|
|
|
|
fn log_user_callback_action(callback_query: &CallbackQuery, action: &str) {
|
|
info!(
|
|
"User {} {}",
|
|
UserHandleAndId::from_user(&callback_query.from),
|
|
action
|
|
);
|
|
}
|
|
|
|
// State transition helper
|
|
async fn transition_to_state(
|
|
dialogue: &NewListingDialogue,
|
|
state: ListingWizardState,
|
|
) -> HandlerResult {
|
|
dialogue.update(state).await?;
|
|
Ok(())
|
|
}
|
|
|
|
// Handle callback query errors
|
|
async fn handle_callback_error(
|
|
bot: &Bot,
|
|
dialogue: NewListingDialogue,
|
|
callback_id: CallbackQueryId,
|
|
) -> HandlerResult {
|
|
if let Err(e) = bot.answer_callback_query(callback_id).await {
|
|
log::warn!("Failed to answer callback query: {}", e);
|
|
}
|
|
dialogue.exit().await?;
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// KEYBOARD CREATION UTILITIES
|
|
// ============================================================================
|
|
|
|
// Create a simple single-button keyboard
|
|
fn create_single_button_keyboard(text: &str, callback_data: &str) -> InlineKeyboardMarkup {
|
|
InlineKeyboardMarkup::new([[InlineKeyboardButton::callback(text, callback_data)]])
|
|
}
|
|
|
|
// Create a keyboard with multiple buttons in a single row
|
|
fn create_single_row_keyboard(buttons: &[(&str, &str)]) -> InlineKeyboardMarkup {
|
|
let keyboard_buttons: Vec<InlineKeyboardButton> = buttons
|
|
.iter()
|
|
.map(|(text, callback_data)| InlineKeyboardButton::callback(*text, *callback_data))
|
|
.collect();
|
|
InlineKeyboardMarkup::new([keyboard_buttons])
|
|
}
|
|
|
|
// Create a keyboard with multiple rows
|
|
fn create_multi_row_keyboard(rows: &[&[(&str, &str)]]) -> InlineKeyboardMarkup {
|
|
let mut keyboard = InlineKeyboardMarkup::default();
|
|
for row in rows {
|
|
let buttons: Vec<InlineKeyboardButton> = row
|
|
.iter()
|
|
.map(|(text, callback_data)| InlineKeyboardButton::callback(*text, *callback_data))
|
|
.collect();
|
|
keyboard = keyboard.append_row(buttons);
|
|
}
|
|
keyboard
|
|
}
|
|
|
|
// Create numeric option keyboard (common pattern for slots, duration, etc.)
|
|
fn create_numeric_options_keyboard(options: &[(i32, &str)], prefix: &str) -> InlineKeyboardMarkup {
|
|
let buttons: Vec<InlineKeyboardButton> = options
|
|
.iter()
|
|
.map(|(value, label)| {
|
|
InlineKeyboardButton::callback(*label, format!("{}_{}", prefix, value))
|
|
})
|
|
.collect();
|
|
InlineKeyboardMarkup::new([buttons])
|
|
}
|
|
|
|
// Create duration-specific keyboard
|
|
fn create_duration_keyboard() -> InlineKeyboardMarkup {
|
|
create_single_row_keyboard(&[
|
|
("1 day", "duration_1_day"),
|
|
("3 days", "duration_3_days"),
|
|
("7 days", "duration_7_days"),
|
|
("14 days", "duration_14_days"),
|
|
])
|
|
}
|
|
|
|
// Create slots keyboard
|
|
fn create_slots_keyboard() -> InlineKeyboardMarkup {
|
|
let slots_options = [(1, "1"), (2, "2"), (5, "5"), (10, "10")];
|
|
create_numeric_options_keyboard(&slots_options, "slots")
|
|
}
|
|
|
|
// Create confirmation keyboard (Create/Discard/Edit)
|
|
fn create_confirmation_keyboard() -> InlineKeyboardMarkup {
|
|
create_multi_row_keyboard(&[
|
|
&[
|
|
("✅ Create", "confirm_create"),
|
|
("🗑️ Discard", "confirm_discard"),
|
|
],
|
|
&[("✏️ Edit", "confirm_edit")],
|
|
])
|
|
}
|
|
|
|
// Create field selection keyboard for editing
|
|
fn create_field_selection_keyboard() -> InlineKeyboardMarkup {
|
|
create_multi_row_keyboard(&[
|
|
&[
|
|
("📝 Title", "edit_title"),
|
|
("📄 Description", "edit_description"),
|
|
],
|
|
&[("💰 Price", "edit_price"), ("🔢 Slots", "edit_slots")],
|
|
&[
|
|
("⏰ Start Time", "edit_start_time"),
|
|
("⏱️ Duration", "edit_duration"),
|
|
],
|
|
&[("✅ Done", "edit_done")],
|
|
])
|
|
}
|
|
|
|
// Create start time keyboard
|
|
fn create_start_time_keyboard() -> InlineKeyboardMarkup {
|
|
create_single_button_keyboard("Now", "start_time_now")
|
|
}
|
|
|
|
// Create back button keyboard
|
|
fn create_back_button_keyboard() -> InlineKeyboardMarkup {
|
|
create_single_button_keyboard("🔙 Back", "edit_back")
|
|
}
|
|
|
|
// Create back button with clear option
|
|
fn create_back_button_keyboard_with_clear(field: &str) -> InlineKeyboardMarkup {
|
|
create_single_row_keyboard(&[
|
|
("🔙 Back", "edit_back"),
|
|
(
|
|
&format!("🧹 Clear {}", field),
|
|
&format!("edit_clear_{}", field),
|
|
),
|
|
])
|
|
}
|
|
|
|
// Create skip button keyboard
|
|
fn create_skip_keyboard() -> InlineKeyboardMarkup {
|
|
create_single_button_keyboard("Skip", "skip")
|
|
}
|
|
|
|
// ============================================================================
|
|
// HANDLER TREE AND MAIN FUNCTIONS
|
|
// ============================================================================
|
|
|
|
// Create the dialogue handler tree for new listing wizard
|
|
pub fn new_listing_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
|
dptree::entry()
|
|
.branch(
|
|
Update::filter_message()
|
|
.enter_dialogue::<Message, SqliteStorage<Json>, ListingWizardState>()
|
|
.branch(dptree::entry().filter_command::<Command>().branch(
|
|
dptree::case![Command::NewListing].endpoint(handle_new_listing_command),
|
|
))
|
|
.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),
|
|
),
|
|
)
|
|
.branch(
|
|
Update::filter_callback_query()
|
|
.enter_dialogue::<CallbackQuery, SqliteStorage<Json>, ListingWizardState>()
|
|
.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::AwaitingDuration(state)]
|
|
.endpoint(handle_duration_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),
|
|
),
|
|
)
|
|
}
|
|
|
|
// Handle the /newlisting command - starts the dialogue by setting it to Start state
|
|
async fn handle_new_listing_command(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
info!(
|
|
"User {} ({}) started new fixed price listing wizard",
|
|
msg.chat.username().unwrap_or("unknown"),
|
|
msg.chat.id
|
|
);
|
|
|
|
// Initialize the dialogue to Start state
|
|
dialogue.update(ListingWizardState::Start).await?;
|
|
|
|
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):";
|
|
|
|
send_html_message(&bot, msg.chat.id, response, None).await?;
|
|
Ok(())
|
|
}
|
|
|
|
// Handle the /newlisting command - starts the dialogue (called from within dialogue context)
|
|
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):";
|
|
|
|
send_html_message(&bot, msg.chat.id, response, None).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();
|
|
|
|
log_user_action(&msg, "entered title input");
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
match validate_title(text) {
|
|
Ok(title) => {
|
|
draft.title = title;
|
|
transition_to_state(&dialogue, 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).";
|
|
|
|
send_html_message(&bot, chat_id, response, Some(create_skip_keyboard())).await
|
|
}
|
|
Err(error_msg) => send_html_message(&bot, chat_id, &error_msg, None).await,
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
log_user_action(&msg, "entered description input");
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
if text.len() > 1000 {
|
|
let error_msg = "❌ Description is too long (max 1000 characters). Please enter a shorter description or 'skip':";
|
|
return send_html_message(&bot, chat_id, error_msg, None).await;
|
|
}
|
|
|
|
draft.description = Some(text.to_string());
|
|
transition_to_state(&dialogue, 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>";
|
|
|
|
send_html_message(&bot, chat_id, response, None).await
|
|
}
|
|
|
|
pub async fn handle_description_callback(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
draft: ListingDraft,
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let (data, _chat_id) = match extract_callback_data(&bot, &callback_query).await {
|
|
Ok(result) => result,
|
|
Err(early_return) => return early_return,
|
|
};
|
|
|
|
if data == "skip" {
|
|
log_user_callback_action(&callback_query, "skipped description");
|
|
|
|
transition_to_state(&dialogue, 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;
|
|
edit_html_message(&bot, chat_id, message.id(), &response, None).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_slots_callback(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
draft: ListingDraft,
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let (data, chat_id) = match extract_callback_data(&bot, &callback_query).await {
|
|
Ok(result) => result,
|
|
Err(early_return) => return early_return,
|
|
};
|
|
|
|
if data.starts_with("slots_") {
|
|
log_user_callback_action(&callback_query, &format!("selected slots button: {}", 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>() {
|
|
// 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 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)"
|
|
);
|
|
|
|
send_html_message(&bot, chat_id, &response, Some(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.";
|
|
|
|
edit_html_message(&bot, chat_id, message.id(), &response, None).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.";
|
|
|
|
edit_html_message(&bot, chat_id, message.id(), &response, None).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 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
|
|
);
|
|
|
|
send_html_message(&bot, chat_id, &response, Some(create_duration_keyboard())).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();
|
|
|
|
log_user_action(&msg, "entered price input");
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
match validate_price(text) {
|
|
Ok(price) => {
|
|
draft.buy_now_price = price;
|
|
transition_to_state(&dialogue, ListingWizardState::AwaitingSlots(draft.clone()))
|
|
.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):",
|
|
draft.buy_now_price
|
|
);
|
|
|
|
send_html_message(&bot, chat_id, &response, Some(create_slots_keyboard())).await?
|
|
}
|
|
Err(error_msg) => send_html_message(&bot, chat_id, &error_msg, None).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();
|
|
|
|
log_user_action(&msg, "entered slots input");
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
match validate_slots(text) {
|
|
Ok(slots) => {
|
|
process_slots_and_respond(&bot, dialogue, draft, chat_id, slots).await?;
|
|
}
|
|
Err(error_msg) => {
|
|
send_html_message(&bot, chat_id, &error_msg, None).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();
|
|
|
|
log_user_action(&msg, "entered start time input");
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
match validate_start_time(text) {
|
|
Ok(hours) => {
|
|
process_start_time_and_respond(&bot, dialogue, draft, chat_id, hours).await?;
|
|
}
|
|
Err(error_msg) => {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
&error_msg,
|
|
Some(create_start_time_keyboard()),
|
|
)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_duration_input(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
draft: ListingDraft,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat_id = msg.chat.id;
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
log_user_action(&msg, "entered duration input");
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, msg).await;
|
|
}
|
|
|
|
match validate_duration(text) {
|
|
Ok(duration) => {
|
|
process_duration_and_respond(bot, dialogue, draft, chat_id, duration).await?;
|
|
}
|
|
Err(error_msg) => {
|
|
send_html_message(&bot, chat_id, &error_msg, None).await?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_duration_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(()),
|
|
};
|
|
let chat_id = match callback_query.message {
|
|
Some(message) => message.chat().id,
|
|
_ => return Ok(()),
|
|
};
|
|
|
|
let days = match data {
|
|
"duration_1_day" => 1,
|
|
"duration_3_days" => 3,
|
|
"duration_7_days" => 7,
|
|
"duration_14_days" => 14,
|
|
_ => {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ Invalid duration. Please enter number of days (1-14):",
|
|
None,
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
process_duration_and_respond(bot, dialogue, draft, chat_id, days).await
|
|
}
|
|
|
|
async fn process_duration_and_respond(
|
|
bot: Bot,
|
|
dialogue: NewListingDialogue,
|
|
mut draft: ListingDraft,
|
|
chat_id: ChatId,
|
|
duration: i32,
|
|
) -> HandlerResult {
|
|
draft.duration_hours = duration;
|
|
dialogue
|
|
.update(ListingWizardState::ViewingDraft(draft.clone()))
|
|
.await?;
|
|
|
|
show_confirmation(bot, chat_id, draft).await
|
|
}
|
|
|
|
async fn show_confirmation(bot: Bot, chat_id: ChatId, state: ListingDraft) -> HandlerResult {
|
|
let description_text = state
|
|
.description
|
|
.as_deref()
|
|
.unwrap_or("<i>No description</i>");
|
|
|
|
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" }
|
|
);
|
|
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
&response,
|
|
Some(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("<i>No description</i>");
|
|
|
|
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" }
|
|
);
|
|
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
&response,
|
|
Some(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 {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"Please confirm your choice:",
|
|
Some(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
|
|
);
|
|
|
|
send_html_message(&bot, chat_id, &response, None).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);
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ <b>Error:</b> Failed to create listing. Please try again later.",
|
|
None,
|
|
)
|
|
.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?;
|
|
send_html_message(&bot, msg.chat.id, "❌ Listing creation cancelled.", None).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() {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ Title cannot be empty. Please enter a valid title:",
|
|
Some(create_back_button_keyboard()),
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
|
|
if text.len() > 100 {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ Title too long. Please enter a title with 100 characters or less:",
|
|
Some(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?;
|
|
|
|
send_html_message(&bot, chat_id, "✅ Title updated!", None).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() {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ Please enter a description or type 'none' for no description:",
|
|
Some(create_back_button_keyboard_with_clear("description")),
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
} else if text.len() > 500 {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ Description too long. Please enter a description with 500 characters or less:",
|
|
Some(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?;
|
|
|
|
send_html_message(&bot, chat_id, "✅ Description updated!", None).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(_) => {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ Invalid price format. Please enter a price between $0.01 and $10,000.00 (e.g., 25.99):",
|
|
Some(create_back_button_keyboard()),
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
},
|
|
_ => {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ Invalid price. Please enter a price between $0.01 and $10,000.00 (e.g., 25.99):",
|
|
Some(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?;
|
|
|
|
send_html_message(&bot, chat_id, "✅ Price updated!", None).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,
|
|
_ => {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ Invalid number. Please enter a number between 1 and 1000:",
|
|
Some(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?;
|
|
|
|
send_html_message(&bot, chat_id, "✅ Slots updated!", None).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,
|
|
_ => {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ Invalid number. Please enter hours from now (0-168):",
|
|
Some(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?;
|
|
|
|
send_html_message(&bot, chat_id, "✅ Start time updated!", None).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,
|
|
_ => {
|
|
send_html_message(
|
|
&bot,
|
|
chat_id,
|
|
"❌ Invalid number. Please enter duration in hours (1-720):",
|
|
Some(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?;
|
|
|
|
send_html_message(&bot, chat_id, "✅ Duration updated!", None).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
|
|
);
|
|
|
|
edit_html_message(
|
|
&bot,
|
|
chat_id,
|
|
message.id(),
|
|
&response,
|
|
Some(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("<i>No description</i>");
|
|
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
|
|
);
|
|
|
|
edit_html_message(
|
|
&bot,
|
|
chat_id,
|
|
message.id(),
|
|
&response,
|
|
Some(create_back_button_keyboard()),
|
|
)
|
|
.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
|
|
);
|
|
|
|
edit_html_message(
|
|
&bot,
|
|
chat_id,
|
|
message.id(),
|
|
&response,
|
|
Some(create_back_button_keyboard()),
|
|
)
|
|
.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
|
|
);
|
|
|
|
edit_html_message(
|
|
&bot,
|
|
chat_id,
|
|
message.id(),
|
|
&response,
|
|
Some(create_back_button_keyboard()),
|
|
)
|
|
.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
|
|
);
|
|
|
|
edit_html_message(
|
|
&bot,
|
|
chat_id,
|
|
message.id(),
|
|
&response,
|
|
Some(create_back_button_keyboard()),
|
|
)
|
|
.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" }
|
|
);
|
|
|
|
edit_html_message(
|
|
&bot,
|
|
chat_id,
|
|
message.id(),
|
|
&response,
|
|
Some(create_back_button_keyboard()),
|
|
)
|
|
.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(())
|
|
}
|