Files
pawctioneer-bot/src/commands/new_listing/handlers.rs
2025-09-09 01:40:36 +00:00

323 lines
11 KiB
Rust

//! Main handler functions for the new listing wizard
//!
//! This module contains the primary handler functions that process
//! user input and manage the listing creation workflow.
use crate::{
commands::{
my_listings::enter_my_listings,
new_listing::{
field_processing::{transition_to_field, update_field_on_draft},
keyboard::{
ConfirmationKeyboardButtons, DurationKeyboardButtons,
FieldSelectionKeyboardButtons, SlotsKeyboardButtons, StartTimeKeyboardButtons,
},
messages::{
get_edit_success_message, get_keyboard_for_field, get_listing_type_keyboard,
get_listing_type_selection_message, get_next_field, get_step_message,
get_success_message, step_for_field,
},
types::{ListingDraft, ListingField, NewListingState},
ui::{display_listing_summary, enter_confirm_save_listing},
validations::SetFieldError,
},
},
db::{
listing::{NewListing, PersistedListing},
user::PersistedUser,
ListingDAO,
},
message_utils::*,
App, BotError, BotResult, DialogueRootState, RootDialogue,
};
use anyhow::{anyhow, Context};
use log::info;
use teloxide::{prelude::*, types::*};
/// Handle the /newlisting command - starts the dialogue
pub(super) async fn handle_new_listing_command(app: App, dialogue: RootDialogue) -> BotResult {
enter_select_new_listing_type(app, dialogue).await?;
Ok(())
}
pub async fn enter_select_new_listing_type(app: App, dialogue: RootDialogue) -> BotResult {
// Initialize the dialogue to listing type selection state
dialogue
.update(NewListingState::SelectingListingType)
.await
.context("failed to update dialogue")?;
app.bot
.send_html_message(
get_listing_type_selection_message().to_string(),
Some(get_listing_type_keyboard()),
)
.await?;
Ok(())
}
/// Handle text input for any field during creation
pub async fn handle_awaiting_draft_field_input(
app: App,
dialogue: RootDialogue,
field: ListingField,
mut draft: ListingDraft,
msg: Message,
) -> BotResult {
info!("User entered input step: {field:?}");
// Process the field update
match update_field_on_draft(field, &mut draft, msg.text()) {
Ok(()) => (),
Err(SetFieldError::ValidationFailed(e)) => {
return Err(BotError::user_visible(e));
}
Err(SetFieldError::UnsupportedFieldForListingType) => {
return Err(anyhow!("Cannot update field {field:?} for listing type").into());
}
Err(SetFieldError::FieldRequired) => {
return Err(anyhow!("Cannot update field {field:?} on existing listing").into());
}
};
// Handle final step or transition to next
if let Some(next_field) = get_next_field(field, draft.listing_type()) {
let response = format!(
"{}\n\n{}",
get_success_message(field, draft.listing_type()),
get_step_message(next_field, draft.listing_type())
);
transition_to_field(dialogue, next_field, draft).await?;
app.bot
.send_html_message(response, get_keyboard_for_field(next_field))
.await?;
} else {
// Final step - go to confirmation
enter_confirm_save_listing(app, dialogue, draft, None).await?;
}
Ok(())
}
/// Handle text input for field editing
pub async fn handle_editing_field_input(
app: App,
dialogue: RootDialogue,
field: ListingField,
mut draft: ListingDraft,
msg: Message,
) -> BotResult {
info!("User editing field {field:?}");
// Process the field update
match update_field_on_draft(field, &mut draft, msg.text()) {
Ok(()) => (),
Err(SetFieldError::ValidationFailed(e)) => {
return Err(BotError::user_visible(e));
}
Err(SetFieldError::UnsupportedFieldForListingType) => {
return Err(anyhow!("Cannot update field {field:?} for listing type").into());
}
Err(SetFieldError::FieldRequired) => {
return Err(anyhow!("Cannot update field {field:?} on existing listing").into());
}
};
let flash = get_edit_success_message(field, draft.listing_type());
enter_edit_listing_draft(app, draft, dialogue, Some(flash)).await?;
Ok(())
}
/// Handle viewing draft confirmation callbacks
pub async fn handle_viewing_draft_callback(
app: App,
dialogue: RootDialogue,
draft: ListingDraft,
user: PersistedUser,
callback_query: CallbackQuery,
) -> BotResult {
let data = extract_callback_data(&app.bot, callback_query).await?;
match ConfirmationKeyboardButtons::try_from(data.as_str())? {
ConfirmationKeyboardButtons::Create | ConfirmationKeyboardButtons::Save => {
info!("User confirmed listing creation");
let success_message = save_listing(&app.daos.listing, draft).await?;
enter_my_listings(app, dialogue, user, Some(success_message)).await?;
}
ConfirmationKeyboardButtons::Cancel => {
info!("User cancelled listing update");
let response = "🗑️ <b>Changes Discarded</b>\n\n\
Your changes have been discarded and not saved."
.to_string();
app.bot.send_html_message(response, None).await?;
dialogue.exit().await.context("failed to exit dialogue")?;
}
ConfirmationKeyboardButtons::Discard => {
info!("User discarded listing creation");
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."
.to_string();
app.bot.send_html_message(response, None).await?;
dialogue.exit().await.context("failed to exit dialogue")?;
}
ConfirmationKeyboardButtons::Edit => {
info!("User chose to edit listing");
enter_edit_listing_draft(app, draft, dialogue, None).await?;
}
}
Ok(())
}
/// Handle editing draft field selection callbacks
pub async fn handle_editing_draft_callback(
app: App,
draft: ListingDraft,
dialogue: RootDialogue,
callback_query: CallbackQuery,
) -> BotResult {
let data = extract_callback_data(&app.bot, callback_query).await?;
info!("User in editing screen, showing field selection");
let button = FieldSelectionKeyboardButtons::try_from(data.as_str())?;
if button == FieldSelectionKeyboardButtons::Done {
return enter_confirm_save_listing(app, dialogue, draft, None).await;
}
let field = match button {
FieldSelectionKeyboardButtons::Title => ListingField::Title,
FieldSelectionKeyboardButtons::Description => ListingField::Description,
FieldSelectionKeyboardButtons::Price => ListingField::Price,
FieldSelectionKeyboardButtons::Slots => ListingField::Slots,
FieldSelectionKeyboardButtons::StartTime => ListingField::StartTime,
FieldSelectionKeyboardButtons::Duration => ListingField::EndTime,
FieldSelectionKeyboardButtons::Done => {
return Err(anyhow::anyhow!("Done button should not be used here").into());
}
};
let value = get_current_field_value(&draft, field)?;
let keyboard = get_edit_keyboard_for_field(field);
dialogue
.update(DialogueRootState::NewListing(
NewListingState::EditingDraftField { field, draft },
))
.await
.context("failed to update dialogue")?;
let response = format!("Editing {field:?}\n\nPrevious value: {value}");
app.bot.send_html_message(response, Some(keyboard)).await?;
Ok(())
}
/// Handle editing draft field callbacks (back button, etc.)
pub async fn handle_editing_draft_field_callback(
app: App,
dialogue: RootDialogue,
field: ListingField,
draft: ListingDraft,
callback_query: CallbackQuery,
) -> BotResult {
let data = extract_callback_data(&app.bot, callback_query).await?;
info!("User editing field: {field:?} -> {data:?}");
if data == "edit_back" {
enter_edit_listing_draft(app, draft, dialogue, None).await?;
return Ok(());
}
// This callback handler typically receives button presses, not text input
// For now, just redirect back to edit screen since callback data isn't suitable for validation
enter_edit_listing_draft(app, draft, dialogue, None).await?;
Ok(())
}
/// Enter the edit listing draft screen
pub async fn enter_edit_listing_draft(
app: App,
draft: ListingDraft,
dialogue: RootDialogue,
flash_message: Option<String>,
) -> BotResult {
display_listing_summary(
app,
&draft,
Some(FieldSelectionKeyboardButtons::to_keyboard()),
flash_message,
)
.await?;
dialogue
.update(NewListingState::EditingDraft(draft))
.await
.context("failed to update dialogue")?;
Ok(())
}
/// Save the listing to the database
async fn save_listing(listing_dao: &ListingDAO, draft: ListingDraft) -> BotResult<String> {
let (listing, success_message) = if let Some(fields) = draft.persisted {
let listing = listing_dao
.update_listing(PersistedListing {
persisted: fields,
base: draft.base,
fields: draft.fields,
})
.await?;
(listing, "Listing updated!")
} else {
let listing = listing_dao
.insert_listing(&NewListing {
persisted: (),
base: draft.base,
fields: draft.fields,
})
.await?;
(listing, "Listing created!")
};
Ok(format!("{success_message}: {}", listing.base.title))
}
/// Get the current value of a field for display
fn get_current_field_value(
draft: &ListingDraft,
field: ListingField,
) -> Result<String, anyhow::Error> {
let step = step_for_field(field, draft.listing_type())
.ok_or_else(|| anyhow::anyhow!("Cannot get field value for field {field:?}"))?;
match (step.get_field_value)(draft) {
Ok(value) => Ok(value.unwrap_or_else(|| "(none)".to_string())),
Err(e) => Err(anyhow::anyhow!(
"Cannot get field value for field {field:?}: {e:?}"
)),
}
}
/// Get the edit keyboard for a field
fn get_edit_keyboard_for_field(field: ListingField) -> InlineKeyboardMarkup {
use crate::message_utils::create_single_button_keyboard;
let back_button = create_single_button_keyboard("🔙 Back", "edit_back");
match field {
ListingField::Description => {
let clear_button =
create_single_button_keyboard("🧹 Clear description", "edit_clear_description");
back_button.append_row(clear_button.inline_keyboard[0].clone())
}
ListingField::Slots => {
back_button.append_row(SlotsKeyboardButtons::to_keyboard().inline_keyboard[0].clone())
}
ListingField::StartTime => back_button
.append_row(StartTimeKeyboardButtons::to_keyboard().inline_keyboard[0].clone()),
ListingField::EndTime => back_button
.append_row(DurationKeyboardButtons::to_keyboard().inline_keyboard[0].clone()),
_ => back_button,
}
}