1088 lines
32 KiB
Rust
1088 lines
32 KiB
Rust
mod handler_factory;
|
|
mod keyboard;
|
|
mod types;
|
|
mod validations;
|
|
|
|
use crate::{
|
|
db::{
|
|
listing::{ListingFields, NewListing, PersistedListing},
|
|
ListingDAO, ListingDuration, UserDAO,
|
|
},
|
|
message_utils::*,
|
|
DialogueRootState, HandlerResult, RootDialogue,
|
|
};
|
|
pub use handler_factory::new_listing_handler;
|
|
use keyboard::*;
|
|
use log::{error, info};
|
|
use sqlx::SqlitePool;
|
|
use teloxide::{prelude::*, types::*, Bot};
|
|
pub use types::*;
|
|
use validations::*;
|
|
|
|
fn create_back_button_keyboard_with(other_buttons: InlineKeyboardMarkup) -> InlineKeyboardMarkup {
|
|
other_buttons.append_row([InlineKeyboardButton::callback("🔙 Back", "edit_back")])
|
|
}
|
|
|
|
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}")),
|
|
])
|
|
}
|
|
|
|
fn create_cancel_keyboard() -> InlineKeyboardMarkup {
|
|
create_single_button_keyboard("Cancel", "cancel")
|
|
}
|
|
|
|
fn create_skip_cancel_keyboard() -> InlineKeyboardMarkup {
|
|
create_multi_row_keyboard(&[&[("Skip", "skip"), ("Cancel", "cancel")]])
|
|
}
|
|
|
|
// Handle the /newlisting command - starts the dialogue by setting it to Start state
|
|
async fn handle_new_listing_command(
|
|
db_pool: SqlitePool,
|
|
bot: Bot,
|
|
dialogue: RootDialogue,
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
info!(
|
|
"User {} started new fixed price listing wizard",
|
|
HandleAndId::from_chat(&msg.chat),
|
|
);
|
|
let user = msg.from.ok_or_else(|| anyhow::anyhow!("User not found"))?;
|
|
let user = UserDAO::find_or_create_by_telegram_user(&db_pool, user).await?;
|
|
|
|
// Initialize the dialogue to Start state
|
|
dialogue
|
|
.update(NewListingState::AwaitingDraftField {
|
|
field: ListingField::Title,
|
|
draft: ListingDraft::draft_for_seller(user.persisted.id),
|
|
})
|
|
.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_message(&bot, msg.chat, response, Some(create_cancel_keyboard())).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_awaiting_draft_field_input(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
(field, draft): (ListingField, ListingDraft),
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat = msg.chat.clone();
|
|
let text = msg.text().unwrap_or("");
|
|
|
|
info!(
|
|
"User {} entered input step: {:?}",
|
|
HandleAndId::from_chat(&chat),
|
|
field
|
|
);
|
|
|
|
if is_cancel(text) {
|
|
return cancel_wizard(bot, dialogue, chat).await;
|
|
}
|
|
|
|
match field {
|
|
ListingField::Title => handle_title_input(bot, chat, text, dialogue, draft).await,
|
|
ListingField::Description => {
|
|
handle_description_input(bot, chat, text, dialogue, draft).await
|
|
}
|
|
ListingField::Price => handle_price_input(bot, chat, text, dialogue, draft).await,
|
|
ListingField::Slots => handle_slots_input(bot, chat, text, dialogue, draft).await,
|
|
ListingField::StartTime => handle_start_time_input(bot, chat, text, dialogue, draft).await,
|
|
ListingField::Duration => handle_duration_input(bot, chat, text, dialogue, draft).await,
|
|
}
|
|
}
|
|
|
|
async fn handle_title_input(
|
|
bot: &Bot,
|
|
chat: Chat,
|
|
text: &str,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
) -> HandlerResult {
|
|
match validate_title(text) {
|
|
Ok(title) => {
|
|
draft.base.title = title;
|
|
dialogue
|
|
.update(NewListingState::AwaitingDraftField {
|
|
field: ListingField::Description,
|
|
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_message(&bot, chat, response, Some(create_skip_cancel_keyboard())).await
|
|
}
|
|
Err(error_msg) => send_message(&bot, chat, error_msg, None).await,
|
|
}
|
|
}
|
|
|
|
async fn handle_description_input(
|
|
bot: &Bot,
|
|
chat: Chat,
|
|
text: &str,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
) -> HandlerResult {
|
|
draft.base.description = match validate_description(text) {
|
|
Ok(description) => Some(description),
|
|
Err(error_msg) => {
|
|
send_message(&bot, chat, error_msg, None).await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
dialogue
|
|
.update(NewListingState::AwaitingDraftField {
|
|
field: ListingField::Price,
|
|
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_message(&bot, chat, response, None).await
|
|
}
|
|
|
|
async fn handle_description_callback(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
draft: ListingDraft,
|
|
data: &str,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
match data {
|
|
"skip" => {
|
|
dialogue
|
|
.update(NewListingState::AwaitingDraftField {
|
|
field: ListingField::Price,
|
|
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>";
|
|
|
|
send_message(&bot, target, response, None).await?;
|
|
}
|
|
_ => {
|
|
error!("Unknown callback data: {data}");
|
|
dialogue.exit().await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_awaiting_draft_field_callback(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
(field, draft): (ListingField, ListingDraft),
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
|
info!("User {from:?} selected callback: {data:?}");
|
|
let target = (from, message_id);
|
|
|
|
if data == "cancel" {
|
|
return cancel_wizard(bot, dialogue, target).await;
|
|
}
|
|
|
|
match field {
|
|
ListingField::Title => {
|
|
error!("Unknown callback data: {data}");
|
|
dialogue.exit().await?;
|
|
Ok(())
|
|
}
|
|
ListingField::Description => {
|
|
handle_description_callback(bot, dialogue, draft, data.as_str(), target).await
|
|
}
|
|
ListingField::Price => {
|
|
error!("Unknown callback data: {data}");
|
|
dialogue.exit().await?;
|
|
Ok(())
|
|
}
|
|
ListingField::Slots => {
|
|
handle_slots_callback(bot, dialogue, draft, data.as_str(), target).await
|
|
}
|
|
ListingField::StartTime => {
|
|
handle_start_time_callback(bot, dialogue, draft, data.as_str(), target).await
|
|
}
|
|
ListingField::Duration => {
|
|
handle_duration_callback(bot, dialogue, draft, data.as_str(), target).await
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn handle_slots_callback(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
draft: ListingDraft,
|
|
data: &str,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let button = SlotsKeyboardButtons::try_from(data)
|
|
.map_err(|_| anyhow::anyhow!("Unknown SlotsKeyboardButtons data: {}", data))?;
|
|
let num_slots = match button {
|
|
SlotsKeyboardButtons::OneSlot => 1,
|
|
SlotsKeyboardButtons::TwoSlots => 2,
|
|
SlotsKeyboardButtons::FiveSlots => 5,
|
|
SlotsKeyboardButtons::TenSlots => 10,
|
|
};
|
|
process_slots_and_respond(&bot, dialogue, draft, target, num_slots).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_start_time_callback(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
draft: ListingDraft,
|
|
data: &str,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
let button = StartTimeKeyboardButtons::try_from(data)
|
|
.map_err(|_| anyhow::anyhow!("Unknown StartTimeKeyboardButtons data: {}", data))?;
|
|
let start_time = match button {
|
|
StartTimeKeyboardButtons::Now => ListingDuration::zero(),
|
|
};
|
|
process_start_time_and_respond(&bot, dialogue, draft, target, start_time).await?;
|
|
Ok(())
|
|
}
|
|
|
|
// Helper function to process slots input, update dialogue state, and send response
|
|
async fn process_slots_and_respond(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
target: impl Into<MessageTarget>,
|
|
slots: i32,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
match &mut draft.fields {
|
|
ListingFields::FixedPriceListing {
|
|
slots_available, ..
|
|
} => {
|
|
*slots_available = slots;
|
|
}
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"Unsupported listing type to update slots: {:?}",
|
|
draft.fields
|
|
));
|
|
}
|
|
};
|
|
|
|
// Update dialogue state
|
|
dialogue
|
|
.update(NewListingState::AwaitingDraftField {
|
|
field: ListingField::StartTime,
|
|
draft,
|
|
})
|
|
.await?;
|
|
|
|
// 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 duration to delay (e.g., '2 days' for 2 days from now, '1 hour' for 1 hour from now)\n\
|
|
• Maximum delay: 168 hours (7 days)"
|
|
);
|
|
|
|
send_message(
|
|
bot,
|
|
target,
|
|
&response,
|
|
Some(StartTimeKeyboardButtons::to_keyboard()),
|
|
)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_viewing_draft_callback(
|
|
db_pool: SqlitePool,
|
|
bot: Bot,
|
|
dialogue: RootDialogue,
|
|
draft: ListingDraft,
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
|
let target = (from, message_id);
|
|
|
|
let button = ConfirmationKeyboardButtons::try_from(data.as_str())
|
|
.map_err(|_| anyhow::anyhow!("Unknown ConfirmationKeyboardButtons data: {}", data))?;
|
|
|
|
match button {
|
|
ConfirmationKeyboardButtons::Create => {
|
|
info!("User {target:?} confirmed listing creation");
|
|
dialogue.exit().await?;
|
|
save_listing(db_pool, bot, dialogue, target, draft).await?;
|
|
}
|
|
ConfirmationKeyboardButtons::Discard => {
|
|
info!("User {target:?} discarded listing creation");
|
|
|
|
// Exit dialogue and send cancellation message
|
|
dialogue.exit().await?;
|
|
|
|
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.";
|
|
|
|
send_message(&bot, target, &response, None).await?;
|
|
}
|
|
ConfirmationKeyboardButtons::Edit => {
|
|
info!("User {target:?} chose to edit listing");
|
|
|
|
// Delete the old message and show the edit screen
|
|
show_edit_screen(&bot, target, &draft, None).await?;
|
|
|
|
// Go to editing state to allow user to modify specific fields
|
|
dialogue
|
|
.update(NewListingState::EditingDraft(draft))
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Helper function to process start time input, update dialogue state, and send response
|
|
async fn process_start_time_and_respond(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
target: impl Into<MessageTarget>,
|
|
duration: ListingDuration,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
|
|
// Update dialogue state
|
|
|
|
match &mut draft.persisted {
|
|
ListingDraftPersisted::New(fields) => {
|
|
fields.start_delay = duration;
|
|
}
|
|
ListingDraftPersisted::Persisted(_) => {
|
|
anyhow::bail!("Cannot update start time for persisted listing");
|
|
}
|
|
}
|
|
|
|
dialogue
|
|
.update(NewListingState::AwaitingDraftField {
|
|
field: ListingField::Duration,
|
|
draft,
|
|
})
|
|
.await?;
|
|
|
|
// Generate response message
|
|
let start_msg = format!("in {duration}");
|
|
|
|
let response = format!(
|
|
"✅ Listing will start: <b>{start_msg}</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):"
|
|
);
|
|
|
|
send_message(
|
|
bot,
|
|
target,
|
|
&response,
|
|
Some(DurationKeyboardButtons::to_keyboard()),
|
|
)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_price_input(
|
|
bot: &Bot,
|
|
chat: Chat,
|
|
text: &str,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
) -> HandlerResult {
|
|
match validate_price(text) {
|
|
Ok(price) => {
|
|
match &mut draft.fields {
|
|
ListingFields::FixedPriceListing { buy_now_price, .. } => {
|
|
*buy_now_price = price;
|
|
}
|
|
_ => {
|
|
anyhow::bail!("Cannot update price for non-fixed price listing");
|
|
}
|
|
}
|
|
|
|
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
|
|
);
|
|
|
|
dialogue
|
|
.update(NewListingState::AwaitingDraftField {
|
|
field: ListingField::Slots,
|
|
draft,
|
|
})
|
|
.await?;
|
|
|
|
send_message(
|
|
&bot,
|
|
chat,
|
|
response,
|
|
Some(SlotsKeyboardButtons::to_keyboard()),
|
|
)
|
|
.await?
|
|
}
|
|
Err(error_msg) => send_message(&bot, chat, error_msg, None).await?,
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_slots_input(
|
|
bot: &Bot,
|
|
chat: Chat,
|
|
text: &str,
|
|
dialogue: RootDialogue,
|
|
draft: ListingDraft,
|
|
) -> HandlerResult {
|
|
match validate_slots(text) {
|
|
Ok(slots) => {
|
|
process_slots_and_respond(&bot, dialogue, draft, chat, slots).await?;
|
|
}
|
|
Err(error_msg) => {
|
|
send_message(&bot, chat, error_msg, None).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_start_time_input(
|
|
bot: &Bot,
|
|
chat: Chat,
|
|
text: &str,
|
|
dialogue: RootDialogue,
|
|
draft: ListingDraft,
|
|
) -> HandlerResult {
|
|
match validate_start_time(text) {
|
|
Ok(duration) => {
|
|
process_start_time_and_respond(&bot, dialogue, draft, chat, duration).await?;
|
|
}
|
|
Err(error_msg) => {
|
|
send_message(
|
|
&bot,
|
|
chat,
|
|
error_msg,
|
|
Some(StartTimeKeyboardButtons::to_keyboard()),
|
|
)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_duration_input(
|
|
bot: &Bot,
|
|
chat: Chat,
|
|
text: &str,
|
|
dialogue: RootDialogue,
|
|
draft: ListingDraft,
|
|
) -> HandlerResult {
|
|
match validate_duration(text) {
|
|
Ok(duration) => {
|
|
process_duration_and_respond(bot, dialogue, draft, chat, duration).await?;
|
|
}
|
|
Err(error_msg) => {
|
|
send_message(&bot, chat, error_msg, None).await?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_duration_callback(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
draft: ListingDraft,
|
|
data: &str,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let button = DurationKeyboardButtons::try_from(data).unwrap();
|
|
let duration = ListingDuration::days(match button {
|
|
DurationKeyboardButtons::OneDay => 1,
|
|
DurationKeyboardButtons::ThreeDays => 3,
|
|
DurationKeyboardButtons::SevenDays => 7,
|
|
DurationKeyboardButtons::FourteenDays => 14,
|
|
});
|
|
process_duration_and_respond(bot, dialogue, draft, target, duration).await
|
|
}
|
|
|
|
async fn process_duration_and_respond(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
target: impl Into<MessageTarget>,
|
|
duration: ListingDuration,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
match &mut draft.persisted {
|
|
ListingDraftPersisted::New(fields) => {
|
|
fields.end_delay = duration;
|
|
}
|
|
ListingDraftPersisted::Persisted(_) => {
|
|
anyhow::bail!("Cannot update duration for persisted listing");
|
|
}
|
|
}
|
|
|
|
show_confirmation_screen(bot, target, &draft).await?;
|
|
dialogue
|
|
.update(NewListingState::ViewingDraft(draft))
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn display_listing_summary(
|
|
bot: &Bot,
|
|
target: impl Into<MessageTarget>,
|
|
draft: &ListingDraft,
|
|
keyboard: Option<InlineKeyboardMarkup>,
|
|
flash_message: Option<&str>,
|
|
) -> HandlerResult {
|
|
let mut response_lines = vec![];
|
|
|
|
if let Some(flash_message) = flash_message {
|
|
response_lines.push(flash_message.to_string());
|
|
}
|
|
|
|
response_lines.push("📋 <i><b>Listing Summary</b></i>".to_string());
|
|
response_lines.push("".to_string());
|
|
response_lines.push(format!("<b>Title:</b> {}", draft.base.title));
|
|
response_lines.push(format!(
|
|
"📄 <b>Description:</b> {}",
|
|
draft
|
|
.base
|
|
.description
|
|
.as_deref()
|
|
.unwrap_or("<i>No description</i>")
|
|
));
|
|
|
|
match &draft.fields {
|
|
ListingFields::FixedPriceListing { buy_now_price, .. } => {
|
|
response_lines.push(format!("💰 <b>Buy it Now Price:</b> ${}", buy_now_price));
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
match &draft.persisted {
|
|
ListingDraftPersisted::New(fields) => {
|
|
response_lines.push(format!("<b>Start delay:</b> {}", fields.start_delay));
|
|
response_lines.push(format!("<b>Duration:</b> {}", fields.end_delay));
|
|
}
|
|
ListingDraftPersisted::Persisted(fields) => {
|
|
response_lines.push(format!("<b>Starts on:</b> {}", fields.start_at));
|
|
response_lines.push(format!("<b>Ends on:</b> {}", fields.end_at));
|
|
}
|
|
}
|
|
|
|
response_lines.push("".to_string());
|
|
response_lines.push("Please review your listing and choose an action:".to_string());
|
|
|
|
send_message(&bot, target, response_lines.join("\n"), keyboard).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn show_edit_screen(
|
|
bot: &Bot,
|
|
target: impl Into<MessageTarget>,
|
|
draft: &ListingDraft,
|
|
flash_message: Option<&str>,
|
|
) -> HandlerResult {
|
|
display_listing_summary(
|
|
bot,
|
|
target,
|
|
draft,
|
|
Some(FieldSelectionKeyboardButtons::to_keyboard()),
|
|
flash_message,
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn show_confirmation_screen(
|
|
bot: &Bot,
|
|
target: impl Into<MessageTarget>,
|
|
draft: &ListingDraft,
|
|
) -> HandlerResult {
|
|
display_listing_summary(
|
|
bot,
|
|
target,
|
|
draft,
|
|
Some(ConfirmationKeyboardButtons::to_keyboard()),
|
|
None,
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_editing_field_input(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
(field, draft): (ListingField, ListingDraft),
|
|
msg: Message,
|
|
) -> HandlerResult {
|
|
let chat = msg.chat.clone();
|
|
let text = msg.text().unwrap_or("").trim();
|
|
|
|
info!("User {chat:?} editing field {field:?}");
|
|
|
|
match field {
|
|
ListingField::Title => {
|
|
handle_edit_title(bot, dialogue, draft, text, chat).await?;
|
|
}
|
|
ListingField::Description => {
|
|
handle_edit_description(bot, dialogue, draft, text, chat).await?;
|
|
}
|
|
ListingField::Price => {
|
|
handle_edit_price(bot, dialogue, draft, text, chat).await?;
|
|
}
|
|
ListingField::Slots => {
|
|
handle_edit_slots(bot, dialogue, draft, text, chat).await?;
|
|
}
|
|
ListingField::StartTime => {
|
|
handle_edit_start_time(bot, dialogue, draft, text, chat).await?;
|
|
}
|
|
ListingField::Duration => {
|
|
handle_edit_duration(bot, dialogue, draft, text, chat).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_editing_draft_callback(
|
|
bot: &Bot,
|
|
draft: ListingDraft,
|
|
dialogue: RootDialogue,
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
|
let target = (from, message_id);
|
|
let button = FieldSelectionKeyboardButtons::try_from(data.as_str())
|
|
.map_err(|e| anyhow::anyhow!("Invalid field selection button: {}", e))?;
|
|
|
|
info!("User {target:?} in editing screen, showing field selection");
|
|
|
|
let (field, value, keyboard) = match button {
|
|
FieldSelectionKeyboardButtons::Title => (
|
|
ListingField::Title,
|
|
draft.base.title.clone(),
|
|
create_back_button_keyboard(),
|
|
),
|
|
FieldSelectionKeyboardButtons::Description => (
|
|
ListingField::Description,
|
|
draft
|
|
.base
|
|
.description
|
|
.as_deref()
|
|
.unwrap_or("(no description)")
|
|
.to_string(),
|
|
create_back_button_keyboard_with_clear("description"),
|
|
),
|
|
FieldSelectionKeyboardButtons::Price => (
|
|
ListingField::Price,
|
|
match &draft.fields {
|
|
ListingFields::FixedPriceListing { buy_now_price, .. } => {
|
|
format!("${}", buy_now_price)
|
|
}
|
|
_ => anyhow::bail!("Cannot update price for non-fixed price listing"),
|
|
},
|
|
create_back_button_keyboard(),
|
|
),
|
|
FieldSelectionKeyboardButtons::Slots => (
|
|
ListingField::Slots,
|
|
match &draft.fields {
|
|
ListingFields::FixedPriceListing {
|
|
slots_available, ..
|
|
} => {
|
|
format!("{} slots", slots_available)
|
|
}
|
|
_ => anyhow::bail!("Cannot update slots for non-fixed price listing"),
|
|
},
|
|
create_back_button_keyboard_with(SlotsKeyboardButtons::to_keyboard()),
|
|
),
|
|
FieldSelectionKeyboardButtons::StartTime => (
|
|
ListingField::StartTime,
|
|
match &draft.persisted {
|
|
ListingDraftPersisted::New(fields) => {
|
|
format!("{} hours", fields.start_delay)
|
|
}
|
|
_ => anyhow::bail!("Cannot update start time of an existing listing"),
|
|
},
|
|
create_back_button_keyboard_with(StartTimeKeyboardButtons::to_keyboard()),
|
|
),
|
|
FieldSelectionKeyboardButtons::Duration => (
|
|
ListingField::Duration,
|
|
match &draft.persisted {
|
|
ListingDraftPersisted::New(fields) => fields.end_delay.to_string(),
|
|
_ => anyhow::bail!("Cannot update duration of an existing listing"),
|
|
},
|
|
create_back_button_keyboard_with(DurationKeyboardButtons::to_keyboard()),
|
|
),
|
|
FieldSelectionKeyboardButtons::Done => {
|
|
show_confirmation_screen(bot, target, &draft).await?;
|
|
dialogue
|
|
.update(DialogueRootState::NewListing(
|
|
NewListingState::ViewingDraft(draft),
|
|
))
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
dialogue
|
|
.update(DialogueRootState::NewListing(
|
|
NewListingState::EditingDraftField { field, draft },
|
|
))
|
|
.await?;
|
|
|
|
// update the message to show the edit screen
|
|
let response = format!(
|
|
"Editing {field:?}\n\n\
|
|
Previous value: {value}\
|
|
"
|
|
);
|
|
|
|
send_message(&bot, target, response, Some(keyboard)).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn save_listing(
|
|
db_pool: SqlitePool,
|
|
bot: Bot,
|
|
dialogue: RootDialogue,
|
|
target: impl Into<MessageTarget>,
|
|
draft: ListingDraft,
|
|
) -> HandlerResult {
|
|
let listing: PersistedListing = match draft.persisted {
|
|
ListingDraftPersisted::New(fields) => {
|
|
ListingDAO::insert_listing(
|
|
&db_pool,
|
|
NewListing {
|
|
persisted: fields,
|
|
base: draft.base,
|
|
fields: draft.fields,
|
|
},
|
|
)
|
|
.await?
|
|
}
|
|
ListingDraftPersisted::Persisted(fields) => {
|
|
ListingDAO::update_listing(
|
|
&db_pool,
|
|
PersistedListing {
|
|
persisted: fields,
|
|
base: draft.base,
|
|
fields: draft.fields,
|
|
},
|
|
)
|
|
.await?
|
|
}
|
|
};
|
|
|
|
let response = format!(
|
|
"✅ <b>Listing Created Successfully!</b>\n\n\
|
|
<b>Listing ID:</b> {}\n\
|
|
<b>Title:</b> {}\n\
|
|
Your fixed price listing is now live! 🎉",
|
|
listing.persisted.id, listing.base.title
|
|
);
|
|
|
|
dialogue.exit().await?;
|
|
send_message(&bot, target, response, None).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Individual field editing handlers
|
|
async fn handle_edit_title(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
text: &str,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
info!("User {target:?} editing title: '{text}'");
|
|
|
|
draft.base.title = match validate_title(text) {
|
|
Ok(title) => title,
|
|
Err(error_msg) => {
|
|
send_message(
|
|
&bot,
|
|
target,
|
|
error_msg,
|
|
Some(create_back_button_keyboard_with_clear("title")),
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// Go back to editing listing state
|
|
show_edit_screen(bot, target, &draft, Some("✅ Title updated!")).await?;
|
|
dialogue
|
|
.update(DialogueRootState::NewListing(
|
|
NewListingState::EditingDraft(draft),
|
|
))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_edit_description(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
text: &str,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
info!("User {target:?} editing description: '{text}'");
|
|
|
|
draft.base.description = match validate_description(text) {
|
|
Ok(description) => Some(description),
|
|
Err(error_msg) => {
|
|
send_message(&bot, target, error_msg, None).await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// Go back to editing listing state
|
|
show_edit_screen(bot, target, &draft, Some("✅ Description updated!")).await?;
|
|
dialogue
|
|
.update(DialogueRootState::NewListing(
|
|
NewListingState::EditingDraft(draft),
|
|
))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_edit_price(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
text: &str,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
info!("User {target:?} editing price: '{text}'");
|
|
|
|
let buy_now_price = match &mut draft.fields {
|
|
ListingFields::FixedPriceListing { buy_now_price, .. } => buy_now_price,
|
|
_ => anyhow::bail!("Cannot update price for non-fixed price listing"),
|
|
};
|
|
|
|
*buy_now_price = match validate_price(text) {
|
|
Ok(price) => price,
|
|
Err(error_msg) => {
|
|
send_message(&bot, target, error_msg, None).await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// Go back to editing listing state
|
|
show_edit_screen(bot, target, &draft, Some("✅ Price updated!")).await?;
|
|
|
|
dialogue
|
|
.update(DialogueRootState::NewListing(
|
|
NewListingState::EditingDraft(draft),
|
|
))
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_edit_slots(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
text: &str,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
info!("User {target:?} editing slots: '{text}'");
|
|
|
|
let slots_available = match &mut draft.fields {
|
|
ListingFields::FixedPriceListing {
|
|
slots_available, ..
|
|
} => slots_available,
|
|
_ => anyhow::bail!("Cannot update slots for non-fixed price listing"),
|
|
};
|
|
|
|
*slots_available = match validate_slots(text) {
|
|
Ok(s) => s,
|
|
Err(error_msg) => {
|
|
send_message(&bot, target, error_msg, None).await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
show_edit_screen(bot, target, &draft, Some("✅ Slots updated!")).await?;
|
|
dialogue
|
|
.update(DialogueRootState::NewListing(
|
|
NewListingState::EditingDraft(draft),
|
|
))
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_edit_start_time(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
text: &str,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
info!("User {target:?} editing start time: '{text}'");
|
|
|
|
let fields = match &mut draft.persisted {
|
|
ListingDraftPersisted::New(fields) => fields,
|
|
_ => anyhow::bail!("Cannot update start time of an existing listing"),
|
|
};
|
|
|
|
fields.start_delay = match validate_start_time(text) {
|
|
Ok(h) => h,
|
|
Err(error_msg) => {
|
|
send_message(&bot, target, error_msg, Some(create_back_button_keyboard())).await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// Go back to editing listing state
|
|
show_edit_screen(bot, target, &draft, Some("✅ Start time updated!")).await?;
|
|
dialogue
|
|
.update(DialogueRootState::NewListing(
|
|
NewListingState::EditingDraft(draft),
|
|
))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_edit_duration(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
mut draft: ListingDraft,
|
|
text: &str,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
info!("User {target:?} editing duration: '{text}'");
|
|
|
|
let fields = match &mut draft.persisted {
|
|
ListingDraftPersisted::New(fields) => fields,
|
|
_ => anyhow::bail!("Cannot update duration of an existing listing"),
|
|
};
|
|
|
|
fields.end_delay = match validate_duration(text) {
|
|
Ok(d) => d,
|
|
Err(error_msg) => {
|
|
send_message(&bot, target, error_msg, Some(create_back_button_keyboard())).await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
show_edit_screen(bot, target, &draft, Some("✅ Duration updated!")).await?;
|
|
dialogue
|
|
.update(DialogueRootState::NewListing(
|
|
NewListingState::EditingDraft(draft),
|
|
))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_editing_draft_field_callback(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
(field, draft): (ListingField, ListingDraft),
|
|
callback_query: CallbackQuery,
|
|
) -> HandlerResult {
|
|
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
|
let target = (from, message_id);
|
|
info!("User {:?} editing field: {:?} -> {}", target, field, &data);
|
|
if data == "edit_back" {
|
|
show_edit_screen(bot, target, &draft, None).await?;
|
|
dialogue
|
|
.update(DialogueRootState::NewListing(
|
|
NewListingState::EditingDraft(draft),
|
|
))
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
|
|
match field {
|
|
ListingField::Title => {
|
|
handle_edit_title(bot, dialogue, draft, data.as_str(), target).await?;
|
|
}
|
|
ListingField::Description => {
|
|
handle_edit_description(bot, dialogue, draft, data.as_str(), target).await?;
|
|
}
|
|
ListingField::Price => {
|
|
handle_edit_price(bot, dialogue, draft, data.as_str(), target).await?;
|
|
}
|
|
ListingField::Slots => {
|
|
handle_edit_slots(bot, dialogue, draft, data.as_str(), target).await?;
|
|
}
|
|
ListingField::StartTime => {
|
|
handle_edit_start_time(bot, dialogue, draft, data.as_str(), target).await?;
|
|
}
|
|
ListingField::Duration => {
|
|
handle_edit_duration(bot, dialogue, draft, data.as_str(), target).await?;
|
|
}
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn cancel_wizard(
|
|
bot: &Bot,
|
|
dialogue: RootDialogue,
|
|
target: impl Into<MessageTarget>,
|
|
) -> HandlerResult {
|
|
let target = target.into();
|
|
info!("{target:?} cancelled new listing wizard");
|
|
dialogue.exit().await?;
|
|
send_message(&bot, target, "❌ Listing creation cancelled.", None).await?;
|
|
Ok(())
|
|
}
|