323 lines
11 KiB
Rust
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,
|
|
}
|
|
}
|