move target into state

This commit is contained in:
Dylan Knutson
2025-09-05 22:17:30 +00:00
parent af5b8883af
commit a40ac5f345
14 changed files with 134 additions and 205 deletions

View File

@@ -12,7 +12,6 @@ use crate::{
dptree_utils::MapTwo,
handle_error::with_error_handler,
handler_utils::find_listing_by_id,
message_utils::MessageTarget,
start_command_data::StartCommandData,
App, BotError, BotHandler, BotResult, DialogueRootState, RootDialogue,
};
@@ -75,7 +74,6 @@ pub fn bidding_handler() -> BotHandler {
async fn handle_place_bid_on_listing(
app: App,
user_dao: UserDAO,
target: MessageTarget,
user: PersistedUser,
listing: PersistedListing,
dialogue: RootDialogue,
@@ -116,7 +114,7 @@ async fn handle_place_bid_on_listing(
.append_row([InlineKeyboardButton::callback("Bid $1", "cancel")]);
app.bot
.send_html_message(target, response_lines.join("\n"), Some(keyboard))
.send_html_message(response_lines.join("\n"), Some(keyboard))
.await?;
Ok(())
@@ -125,7 +123,6 @@ async fn handle_place_bid_on_listing(
async fn handle_awaiting_bid_amount_input(
app: App,
listing: PersistedListing,
target: MessageTarget,
dialogue: RootDialogue,
msg: Message,
) -> BotResult {
@@ -146,7 +143,6 @@ async fn handle_awaiting_bid_amount_input(
let bid_amount_str = format!("{}{}", listing.base.currency_type.symbol(), bid_amount);
app.bot
.send_html_message(
target,
format!("Confirm bid amount: {bid_amount_str} - this cannot be undone!"),
Some(InlineKeyboardMarkup::default().append_row([
InlineKeyboardButton::callback(
@@ -174,7 +170,6 @@ async fn handle_awaiting_confirm_bid_amount_callback(
listing: PersistedListing,
user: PersistedUser,
bid_amount: MoneyAmount,
target: MessageTarget,
dialogue: RootDialogue,
callback_query: CallbackQuery,
) -> BotResult {
@@ -188,7 +183,7 @@ async fn handle_awaiting_confirm_bid_amount_callback(
"cancel_bid" => {
dialogue.exit().await.context("failed to exit dialogue")?;
app.bot
.send_html_message(target, "Bid cancelled".to_string(), None)
.send_html_message("Bid cancelled".to_string(), None)
.await?;
return Ok(());
}
@@ -206,8 +201,8 @@ async fn handle_awaiting_confirm_bid_amount_callback(
let bid_amount_str = format!("{}{}", listing.base.currency_type.symbol(), bid_amount);
app.bot
.with_target(callback_query.from.into())
.send_html_message(
target.only_chat_id(),
format!("Bid placed for {bid_amount_str} on {}", listing.base.title),
None,
)

View File

@@ -1,7 +1,7 @@
use crate::{message_utils::MessageTarget, App, BotResult, Command};
use crate::{App, BotResult, Command};
use teloxide::utils::command::BotCommands;
pub async fn handle_help(app: App, target: MessageTarget) -> BotResult {
pub async fn handle_help(app: App) -> BotResult {
let help_message = format!(
"📋 Available Commands:\n\n{}\n\n\
📧 Support: Contact @admin for help\n\
@@ -9,8 +9,6 @@ pub async fn handle_help(app: App, target: MessageTarget) -> BotResult {
Command::descriptions()
);
app.bot
.send_html_message(target, help_message, None)
.await?;
app.bot.send_html_message(help_message, None).await?;
Ok(())
}

View File

@@ -1,8 +1,8 @@
use crate::{message_utils::MessageTarget, App, BotResult};
use crate::{App, BotResult};
use log::info;
use teloxide::types::Message;
pub async fn handle_my_bids(app: App, msg: Message, target: MessageTarget) -> BotResult {
pub async fn handle_my_bids(app: App, msg: Message) -> BotResult {
let response = "🎯 My Bids (Coming Soon)\n\n\
Here you'll be able to view:\n\
• Your active bids\n\
@@ -18,6 +18,6 @@ pub async fn handle_my_bids(app: App, msg: Message, target: MessageTarget) -> Bo
msg.chat.id
);
app.bot.send_html_message(target, response, None).await?;
app.bot.send_html_message(response, None).await?;
Ok(())
}

View File

@@ -19,7 +19,7 @@ use crate::{
},
handle_error::with_error_handler,
handler_utils::{find_listing_by_id, find_or_create_db_user_from_update},
message_utils::{extract_callback_data, pluralize_with_count, MessageTarget},
message_utils::{extract_callback_data, pluralize_with_count},
start_command_data::StartCommandData,
App, BotError, BotResult, Command, DialogueRootState, RootDialogue,
};
@@ -87,12 +87,8 @@ pub fn my_listings_handler() -> Handler<'static, BotResult, DpHandlerDescription
)
}
async fn handle_view_listing_details(
app: App,
listing: PersistedListing,
target: MessageTarget,
) -> BotResult {
send_listing_details_message(app, target, listing, None).await?;
async fn handle_view_listing_details(app: App, listing: PersistedListing) -> BotResult {
send_listing_details_message(app, listing, None).await?;
Ok(())
}
@@ -230,9 +226,8 @@ async fn handle_my_listings_command_input(
app: App,
dialogue: RootDialogue,
user: PersistedUser,
target: MessageTarget,
) -> BotResult {
enter_my_listings(app, dialogue, user, target, None).await?;
enter_my_listings(app, dialogue, user, None).await?;
Ok(())
}
@@ -240,7 +235,6 @@ pub async fn enter_my_listings(
app: App,
dialogue: RootDialogue,
user: PersistedUser,
target: MessageTarget,
flash: Option<String>,
) -> BotResult {
// Transition to ViewingListings state
@@ -263,7 +257,6 @@ pub async fn enter_my_listings(
if listings.is_empty() {
app.bot
.send_html_message(
target,
"📋 <b>My Listings</b>\n\n\
You don't have any listings yet."
.to_string(),
@@ -284,9 +277,7 @@ pub async fn enter_my_listings(
response = format!("{flash}\n\n{response}");
}
app.bot
.send_html_message(target, response, Some(keyboard))
.await?;
app.bot.send_html_message(response, Some(keyboard)).await?;
Ok(())
}
@@ -295,12 +286,11 @@ async fn handle_viewing_listings_callback(
dialogue: RootDialogue,
callback_query: CallbackQuery,
user: PersistedUser,
target: MessageTarget,
) -> BotResult {
let data = extract_callback_data(app.bot.deref(), callback_query).await?;
if let Ok(NavKeyboardButtons::Back) = NavKeyboardButtons::try_from(data.as_str()) {
return enter_main_menu(app, dialogue, target).await;
return enter_main_menu(app, dialogue).await;
}
// Check if it's the back to menu button
@@ -308,10 +298,10 @@ async fn handle_viewing_listings_callback(
match button {
MyListingsButtons::SelectListing(listing_id) => {
let listing = get_listing_for_user(&app.daos, user, listing_id).await?;
enter_show_listing_details(app, dialogue, listing, target).await?;
enter_show_listing_details(app, dialogue, listing).await?;
}
MyListingsButtons::NewListing => {
enter_select_new_listing_type(app, dialogue, target).await?;
enter_select_new_listing_type(app, dialogue).await?;
}
}
@@ -322,7 +312,6 @@ async fn enter_show_listing_details(
app: App,
dialogue: RootDialogue,
listing: PersistedListing,
target: MessageTarget,
) -> BotResult {
let listing_id = listing.persisted.id;
dialogue
@@ -342,13 +331,12 @@ async fn enter_show_listing_details(
ManageListingButtons::Delete.to_button(),
])
.append_row([ManageListingButtons::Back.to_button()]);
send_listing_details_message(app, target, listing, Some(keyboard)).await?;
send_listing_details_message(app, listing, Some(keyboard)).await?;
Ok(())
}
async fn send_listing_details_message(
app: App,
target: MessageTarget,
listing: PersistedListing,
keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult {
@@ -365,7 +353,7 @@ async fn send_listing_details_message(
response_lines.push(format!("<b>{}:</b> {}", step.field_name, field_value));
}
app.bot
.send_html_message(target, response_lines.join("\n"), keyboard)
.send_html_message(response_lines.join("\n"), keyboard)
.await?;
Ok(())
}
@@ -376,7 +364,6 @@ async fn handle_managing_listing_callback(
callback_query: CallbackQuery,
user: PersistedUser,
listing_id: ListingDbId,
target: MessageTarget,
) -> BotResult {
let from = callback_query.from.clone();
let data = extract_callback_data(&app.bot, callback_query).await?;
@@ -397,21 +384,14 @@ async fn handle_managing_listing_callback(
ManageListingButtons::Edit => {
let listing = get_listing_for_user(&app.daos, user, listing_id).await?;
let draft = ListingDraft::from_persisted(listing);
enter_edit_listing_draft(app, target, draft, dialogue, None).await?;
enter_edit_listing_draft(app, draft, dialogue, None).await?;
}
ManageListingButtons::Delete => {
app.daos.listing.delete_listing(listing_id).await?;
enter_my_listings(
app,
dialogue,
user,
target,
Some("Listing deleted.".to_string()),
)
.await?;
enter_my_listings(app, dialogue, user, Some("Listing deleted.".to_string())).await?;
}
ManageListingButtons::Back => {
enter_my_listings(app, dialogue, user, target, None).await?;
enter_my_listings(app, dialogue, user, None).await?;
}
}
@@ -459,9 +439,10 @@ async fn send_preview_listing_message(
if let Some(description) = &listing.base.description {
response_lines.push(description.to_owned());
}
app.bot
.with_target(from.into())
.send_html_message(
from.into(),
response_lines.join("\n\n"),
Some(keyboard_for_listing(&listing)),
)

View File

@@ -28,13 +28,12 @@ pub async fn handle_selecting_listing_type_callback(
dialogue: RootDialogue,
user: PersistedUser,
callback_query: CallbackQuery,
target: MessageTarget,
) -> BotResult {
let data = extract_callback_data(&app.bot, callback_query).await?;
info!("User {target:?} selected listing type: {data:?}");
info!("User selected listing type: {data:?}");
if let Ok(NavKeyboardButtons::Back) = NavKeyboardButtons::try_from(data.as_str()) {
return enter_my_listings(app, dialogue, user, target, None).await;
return enter_my_listings(app, dialogue, user, None).await;
}
// Parse the listing type from callback data
@@ -62,11 +61,7 @@ pub async fn handle_selecting_listing_type_callback(
);
app.bot
.send_html_message(
target,
response,
get_keyboard_for_field(ListingField::Title),
)
.send_html_message(response, get_keyboard_for_field(ListingField::Title))
.await?;
Ok(())
@@ -79,21 +74,20 @@ pub async fn handle_awaiting_draft_field_callback(
field: ListingField,
draft: ListingDraft,
callback_query: CallbackQuery,
target: MessageTarget,
) -> BotResult {
let data = extract_callback_data(&app.bot, callback_query).await?;
info!("User {target:?} selected callback: {data:?}");
info!("User selected callback: {data:?}");
if let Ok(button) = NavKeyboardButtons::try_from(data.as_str()) {
match button {
NavKeyboardButtons::Back => {
return enter_select_new_listing_type(app, dialogue, target).await;
return enter_select_new_listing_type(app, dialogue).await;
}
NavKeyboardButtons::Skip => {
return handle_skip_field(app, dialogue, field, draft, target).await;
return handle_skip_field(app, dialogue, field, draft).await;
}
NavKeyboardButtons::Cancel => {
return cancel_wizard(app, dialogue, target).await;
return cancel_wizard(app, dialogue).await;
}
}
}
@@ -102,23 +96,23 @@ pub async fn handle_awaiting_draft_field_callback(
match field {
ListingField::Slots => {
let button = SlotsKeyboardButtons::try_from(data.as_str())?;
handle_slots_callback(app, dialogue, draft, button, target).await
handle_slots_callback(app, dialogue, draft, button).await
}
ListingField::StartTime => {
let button = StartTimeKeyboardButtons::try_from(data.as_str())?;
handle_start_time_callback(app, dialogue, draft, button, target).await
handle_start_time_callback(app, dialogue, draft, button).await
}
ListingField::EndTime => {
let button = DurationKeyboardButtons::try_from(data.as_str())?;
handle_duration_callback(app, dialogue, draft, button, target).await
handle_duration_callback(app, dialogue, draft, button).await
}
ListingField::MinBidIncrement => {
let button = EditMinimumBidIncrementKeyboardButtons::try_from(data.as_str())?;
handle_starting_bid_amount_callback(app, dialogue, draft, button, target).await
handle_starting_bid_amount_callback(app, dialogue, draft, button).await
}
ListingField::CurrencyType => {
let button = CurrencyTypeKeyboardButtons::try_from(data.as_str())?;
handle_currency_type_callback(app, dialogue, draft, button, target).await
handle_currency_type_callback(app, dialogue, draft, button).await
}
_ => {
error!("Unknown callback data for field {field:?}: {data}");
@@ -132,7 +126,6 @@ async fn handle_skip_field(
dialogue: RootDialogue,
current_field: ListingField,
draft: ListingDraft,
target: MessageTarget,
) -> BotResult {
let field_name = get_field_name(current_field, draft.listing_type());
let next_field = get_next_field(current_field, draft.listing_type());
@@ -145,10 +138,10 @@ async fn handle_skip_field(
);
transition_to_field(dialogue, next_field, draft).await?;
app.bot
.send_html_message(target, response, get_keyboard_for_field(next_field))
.send_html_message(response, get_keyboard_for_field(next_field))
.await?;
} else {
enter_confirm_save_listing(app, dialogue, target, draft, Some(flash)).await?;
enter_confirm_save_listing(app, dialogue, draft, Some(flash)).await?;
}
Ok(())
}
@@ -159,7 +152,6 @@ async fn handle_slots_callback(
dialogue: RootDialogue,
mut draft: ListingDraft,
button: SlotsKeyboardButtons,
target: MessageTarget,
) -> BotResult {
let num_slots = match button {
SlotsKeyboardButtons::OneSlot => 1,
@@ -181,11 +173,7 @@ async fn handle_slots_callback(
);
transition_to_field(dialogue, ListingField::StartTime, draft).await?;
app.bot
.send_html_message(
target,
response,
get_keyboard_for_field(ListingField::StartTime),
)
.send_html_message(response, get_keyboard_for_field(ListingField::StartTime))
.await?;
Ok(())
}
@@ -196,7 +184,6 @@ async fn handle_start_time_callback(
dialogue: RootDialogue,
mut draft: ListingDraft,
button: StartTimeKeyboardButtons,
target: MessageTarget,
) -> BotResult {
let start_time = match button {
StartTimeKeyboardButtons::Now => ListingDuration::zero(),
@@ -216,11 +203,7 @@ async fn handle_start_time_callback(
);
transition_to_field(dialogue, ListingField::EndTime, draft).await?;
app.bot
.send_html_message(
target,
response,
get_keyboard_for_field(ListingField::EndTime),
)
.send_html_message(response, get_keyboard_for_field(ListingField::EndTime))
.await?;
Ok(())
}
@@ -231,7 +214,6 @@ async fn handle_duration_callback(
dialogue: RootDialogue,
mut draft: ListingDraft,
button: DurationKeyboardButtons,
target: MessageTarget,
) -> BotResult {
let duration = ListingDuration::days(match button {
DurationKeyboardButtons::OneDay => 1,
@@ -248,7 +230,7 @@ async fn handle_duration_callback(
.map_err(|e| anyhow::anyhow!("Error updating duration: {e:?}"))?;
let flash = get_success_message(ListingField::EndTime, draft.listing_type());
enter_confirm_save_listing(app, dialogue, target, draft, Some(flash)).await
enter_confirm_save_listing(app, dialogue, draft, Some(flash)).await
}
async fn handle_starting_bid_amount_callback(
@@ -256,7 +238,6 @@ async fn handle_starting_bid_amount_callback(
dialogue: RootDialogue,
mut draft: ListingDraft,
button: EditMinimumBidIncrementKeyboardButtons,
target: MessageTarget,
) -> BotResult {
let starting_bid_amount = MoneyAmount::from_str(match button {
EditMinimumBidIncrementKeyboardButtons::OneDollar => "1.00",
@@ -273,7 +254,7 @@ async fn handle_starting_bid_amount_callback(
.map_err(|e| anyhow::anyhow!("Error updating starting bid amount: {e:?}"))?;
let flash = get_success_message(ListingField::StartingBidAmount, draft.listing_type());
enter_confirm_save_listing(app, dialogue, target, draft, Some(flash)).await
enter_confirm_save_listing(app, dialogue, draft, Some(flash)).await
}
async fn handle_currency_type_callback(
@@ -281,7 +262,6 @@ async fn handle_currency_type_callback(
dialogue: RootDialogue,
mut draft: ListingDraft,
button: CurrencyTypeKeyboardButtons,
target: MessageTarget,
) -> BotResult {
let currency_type = match button {
CurrencyTypeKeyboardButtons::Usd => CurrencyType::Usd,
@@ -305,14 +285,14 @@ async fn handle_currency_type_callback(
);
transition_to_field(dialogue, next_field, draft).await?;
app.bot
.send_html_message(target, response, get_keyboard_for_field(next_field))
.send_html_message(response, get_keyboard_for_field(next_field))
.await?;
Ok(())
}
/// Cancel the wizard and exit
pub async fn cancel_wizard(app: App, dialogue: RootDialogue, target: MessageTarget) -> BotResult {
info!("{target:?} cancelled new listing wizard");
enter_select_new_listing_type(app, dialogue, target).await?;
pub async fn cancel_wizard(app: App, dialogue: RootDialogue) -> BotResult {
info!("User cancelled new listing wizard");
enter_select_new_listing_type(app, dialogue).await?;
Ok(())
}

View File

@@ -35,20 +35,12 @@ 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,
target: MessageTarget,
) -> BotResult {
enter_select_new_listing_type(app, dialogue, target).await?;
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,
target: MessageTarget,
) -> BotResult {
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)
@@ -57,7 +49,6 @@ pub async fn enter_select_new_listing_type(
app.bot
.send_html_message(
target,
get_listing_type_selection_message().to_string(),
Some(get_listing_type_keyboard()),
)
@@ -71,10 +62,9 @@ pub async fn handle_awaiting_draft_field_input(
dialogue: RootDialogue,
field: ListingField,
mut draft: ListingDraft,
target: MessageTarget,
msg: Message,
) -> BotResult {
info!("User {target:?} entered input step: {field:?}");
info!("User entered input step: {field:?}");
// Process the field update
match update_field_on_draft(field, &mut draft, msg.text()) {
@@ -99,11 +89,11 @@ pub async fn handle_awaiting_draft_field_input(
);
transition_to_field(dialogue, next_field, draft).await?;
app.bot
.send_html_message(target, response, get_keyboard_for_field(next_field))
.send_html_message(response, get_keyboard_for_field(next_field))
.await?;
} else {
// Final step - go to confirmation
enter_confirm_save_listing(app, dialogue, target, draft, None).await?;
enter_confirm_save_listing(app, dialogue, draft, None).await?;
}
Ok(())
}
@@ -114,10 +104,9 @@ pub async fn handle_editing_field_input(
dialogue: RootDialogue,
field: ListingField,
mut draft: ListingDraft,
target: MessageTarget,
msg: Message,
) -> BotResult {
info!("User {target:?} editing field {field:?}");
info!("User editing field {field:?}");
// Process the field update
match update_field_on_draft(field, &mut draft, msg.text()) {
@@ -134,7 +123,7 @@ pub async fn handle_editing_field_input(
};
let flash = get_edit_success_message(field, draft.listing_type());
enter_edit_listing_draft(app, target, draft, dialogue, Some(flash)).await?;
enter_edit_listing_draft(app, draft, dialogue, Some(flash)).await?;
Ok(())
}
@@ -145,37 +134,36 @@ pub async fn handle_viewing_draft_callback(
draft: ListingDraft,
user: PersistedUser,
callback_query: CallbackQuery,
target: MessageTarget,
) -> BotResult {
let data = extract_callback_data(&app.bot, callback_query).await?;
match ConfirmationKeyboardButtons::try_from(data.as_str())? {
ConfirmationKeyboardButtons::Create | ConfirmationKeyboardButtons::Save => {
info!("User {target:?} confirmed listing creation");
info!("User confirmed listing creation");
let success_message = save_listing(&app.daos.listing, draft).await?;
enter_my_listings(app, dialogue, user, target, Some(success_message)).await?;
enter_my_listings(app, dialogue, user, Some(success_message)).await?;
}
ConfirmationKeyboardButtons::Cancel => {
info!("User {target:?} cancelled listing update");
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(target, response, None).await?;
app.bot.send_html_message(response, None).await?;
dialogue.exit().await.context("failed to exit dialogue")?;
}
ConfirmationKeyboardButtons::Discard => {
info!("User {target:?} discarded listing creation");
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(target, response, None).await?;
app.bot.send_html_message(response, None).await?;
dialogue.exit().await.context("failed to exit dialogue")?;
}
ConfirmationKeyboardButtons::Edit => {
info!("User {target:?} chose to edit listing");
enter_edit_listing_draft(app, target, draft, dialogue, None).await?;
info!("User chose to edit listing");
enter_edit_listing_draft(app, draft, dialogue, None).await?;
}
}
@@ -188,14 +176,13 @@ pub async fn handle_editing_draft_callback(
draft: ListingDraft,
dialogue: RootDialogue,
callback_query: CallbackQuery,
target: MessageTarget,
) -> BotResult {
let data = extract_callback_data(&app.bot, callback_query).await?;
info!("User {target:?} in editing screen, showing field selection");
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, target, draft, None).await;
return enter_confirm_save_listing(app, dialogue, draft, None).await;
}
let field = match button {
@@ -221,9 +208,7 @@ pub async fn handle_editing_draft_callback(
.context("failed to update dialogue")?;
let response = format!("Editing {field:?}\n\nPrevious value: {value}");
app.bot
.send_html_message(target, response, Some(keyboard))
.await?;
app.bot.send_html_message(response, Some(keyboard)).await?;
Ok(())
}
@@ -235,19 +220,18 @@ pub async fn handle_editing_draft_field_callback(
field: ListingField,
draft: ListingDraft,
callback_query: CallbackQuery,
target: MessageTarget,
) -> BotResult {
let data = extract_callback_data(&app.bot, callback_query).await?;
info!("User {target:?} editing field: {field:?} -> {data:?}");
info!("User editing field: {field:?} -> {data:?}");
if data == "edit_back" {
enter_edit_listing_draft(app, target, draft, dialogue, None).await?;
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, target, draft, dialogue, None).await?;
enter_edit_listing_draft(app, draft, dialogue, None).await?;
Ok(())
}
@@ -255,14 +239,12 @@ pub async fn handle_editing_draft_field_callback(
/// Enter the edit listing draft screen
pub async fn enter_edit_listing_draft(
app: App,
target: MessageTarget,
draft: ListingDraft,
dialogue: RootDialogue,
flash_message: Option<String>,
) -> BotResult {
display_listing_summary(
app,
target,
&draft,
Some(FieldSelectionKeyboardButtons::to_keyboard()),
flash_message,

View File

@@ -9,14 +9,13 @@ use crate::commands::new_listing::NewListingState;
use crate::db::ListingType;
use crate::App;
use crate::RootDialogue;
use crate::{commands::new_listing::types::ListingDraft, message_utils::*, BotResult};
use crate::{commands::new_listing::types::ListingDraft, BotResult};
use anyhow::Context;
use teloxide::types::InlineKeyboardMarkup;
/// Display the listing summary with optional flash message and keyboard
pub async fn display_listing_summary(
app: App,
target: MessageTarget,
draft: &ListingDraft,
keyboard: Option<InlineKeyboardMarkup>,
flash_message: Option<String>,
@@ -50,7 +49,7 @@ pub async fn display_listing_summary(
response_lines.push("Edit your listing:".to_string());
app.bot
.send_html_message(target, response_lines.join("\n"), keyboard)
.send_html_message(response_lines.join("\n"), keyboard)
.await?;
Ok(())
@@ -60,7 +59,6 @@ pub async fn display_listing_summary(
pub async fn enter_confirm_save_listing(
app: App,
dialogue: RootDialogue,
target: MessageTarget,
draft: ListingDraft,
flash: Option<String>,
) -> BotResult {
@@ -78,7 +76,7 @@ pub async fn enter_confirm_save_listing(
])
};
display_listing_summary(app, target, &draft, Some(keyboard), flash).await?;
display_listing_summary(app, &draft, Some(keyboard), flash).await?;
dialogue
.update(NewListingState::ViewingDraft(draft))
.await

View File

@@ -1,8 +1,8 @@
use crate::{message_utils::MessageTarget, App, BotResult};
use crate::{App, BotResult};
use log::info;
use teloxide::types::Message;
pub async fn handle_settings(app: App, msg: Message, target: MessageTarget) -> BotResult {
pub async fn handle_settings(app: App, msg: Message) -> BotResult {
let response = "⚙️ Settings (Coming Soon)\n\n\
Here you'll be able to configure:\n\
• Notification preferences\n\
@@ -18,6 +18,6 @@ pub async fn handle_settings(app: App, msg: Message, target: MessageTarget) -> B
msg.chat.id
);
app.bot.send_html_message(target, response, None).await?;
app.bot.send_html_message(response, None).await?;
Ok(())
}

View File

@@ -6,11 +6,8 @@ use teloxide::{
};
use crate::{
commands::my_listings::enter_my_listings,
db::user::PersistedUser,
keyboard_buttons,
message_utils::{extract_callback_data, MessageTarget},
App, BotResult, Command, DialogueRootState, RootDialogue,
commands::my_listings::enter_my_listings, db::user::PersistedUser, keyboard_buttons,
message_utils::extract_callback_data, App, BotResult, Command, DialogueRootState, RootDialogue,
};
keyboard_buttons! {
@@ -37,19 +34,14 @@ fn get_main_menu_message() -> &'static str {
Choose an option below to get started! 🚀"
}
pub async fn handle_start(
app: App,
dialogue: RootDialogue,
target: MessageTarget,
update: Update,
) -> BotResult {
pub async fn handle_start(app: App, dialogue: RootDialogue, update: Update) -> BotResult {
info!("got start message: {update:?}");
enter_main_menu(app, dialogue, target).await?;
enter_main_menu(app, dialogue).await?;
Ok(())
}
/// Show the main menu with buttons
pub async fn enter_main_menu(app: App, dialogue: RootDialogue, target: MessageTarget) -> BotResult {
pub async fn enter_main_menu(app: App, dialogue: RootDialogue) -> BotResult {
dialogue
.update(DialogueRootState::MainMenu)
.await
@@ -57,7 +49,6 @@ pub async fn enter_main_menu(app: App, dialogue: RootDialogue, target: MessageTa
app.bot
.send_html_message(
target,
get_main_menu_message().to_string(),
Some(MainMenuButtons::to_keyboard()),
)
@@ -71,21 +62,19 @@ pub async fn handle_main_menu_callback(
dialogue: RootDialogue,
user: PersistedUser,
callback_query: CallbackQuery,
target: MessageTarget,
) -> BotResult {
let data = extract_callback_data(&app.bot, callback_query).await?;
info!("User {target:?} selected main menu option: {data:?}");
info!("User selected main menu option: {data:?}");
let button = MainMenuButtons::try_from(data.as_str())?;
match button {
MainMenuButtons::MyListings => {
// Call show_listings_for_user directly
enter_my_listings(app, dialogue, user, target, None).await?;
enter_my_listings(app, dialogue, user, None).await?;
}
MainMenuButtons::MyBids => {
app.bot
.send_html_message(
target,
"💰 <b>My Bids (Coming Soon)</b>\n\n\
Here you'll be able to view:\n\
• Your active bids\n\
@@ -101,7 +90,6 @@ pub async fn handle_main_menu_callback(
MainMenuButtons::Settings => {
app.bot
.send_html_message(
target,
"⚙️ <b>Settings (Coming Soon)</b>\n\n\
Here you'll be able to configure:\n\
• Notification preferences\n\
@@ -122,7 +110,7 @@ pub async fn handle_main_menu_callback(
Command::descriptions()
);
app.bot
.send_html_message(target, help_message, Some(MainMenuButtons::to_keyboard()))
.send_html_message(help_message, Some(MainMenuButtons::to_keyboard()))
.await?;
}
}

View File

@@ -1,18 +1,13 @@
use crate::{
message_utils::MessageTarget, wrap_endpoint, App, BotError, BotResult, WrappedAsyncFn,
};
use crate::{wrap_endpoint, App, BotError, BotResult, WrappedAsyncFn};
use futures::future::BoxFuture;
pub async fn handle_error(app: App, target: MessageTarget, error: BotError) -> BotResult {
pub async fn handle_error(app: App, error: BotError) -> BotResult {
log::error!("Error in handler: {error:?}");
match error {
BotError::UserVisibleError(message) => {
app.bot.send_html_message(target, message, None).await?
}
BotError::UserVisibleError(message) => app.bot.send_html_message(message, None).await?,
BotError::InternalError(_) => {
app.bot
.send_html_message(
target,
"An internal error occurred. Please try again later.".to_string(),
None,
)
@@ -22,20 +17,16 @@ pub async fn handle_error(app: App, target: MessageTarget, error: BotError) -> B
Ok(())
}
fn boxed_handle_error(
app: App,
target: MessageTarget,
error: BotError,
) -> BoxFuture<'static, BotResult> {
Box::pin(handle_error(app, target, error))
fn boxed_handle_error(app: App, error: BotError) -> BoxFuture<'static, BotResult> {
Box::pin(handle_error(app, error))
}
pub type ErrorHandlerWrapped<FnBase, FnBaseArgs> = WrappedAsyncFn<
FnBase,
fn(App, MessageTarget, BotError) -> BoxFuture<'static, BotResult>,
fn(App, BotError) -> BoxFuture<'static, BotResult>,
BotError,
FnBaseArgs,
(App, MessageTarget),
(App,),
>;
pub fn with_error_handler<FnBase, FnBaseArgs>(

View File

@@ -25,7 +25,7 @@ use crate::commands::{
use crate::db::DAOs;
use crate::handle_error::with_error_handler;
use crate::handler_utils::{find_or_create_db_user_from_update, update_into_message_target};
use crate::message_sender::BoxMessageSender;
use crate::message_sender::{BotMessageSender, BoxedMessageSender};
use crate::sqlite_storage::SqliteStorage;
use anyhow::Result;
pub use bot_result::*;
@@ -39,12 +39,12 @@ pub use wrap_endpoint::*;
#[derive(Clone)]
pub struct App {
pub bot: Arc<BoxMessageSender>,
pub bot: Arc<BoxedMessageSender>,
pub daos: DAOs,
}
impl App {
pub fn new(bot: BoxMessageSender, daos: DAOs) -> Self {
pub fn new(bot: BoxedMessageSender, daos: DAOs) -> Self {
Self {
bot: Arc::new(bot),
daos,
@@ -100,11 +100,9 @@ type RootDialogue = Dialogue<DialogueRootState, SqliteStorage<Json>>;
pub fn main_handler() -> BotHandler {
dptree::entry()
.map(|app: App| app.daos.clone())
.map(|daos: DAOs| daos.user.clone())
.map(|daos: DAOs| daos.listing.clone())
.map(|daos: DAOs| daos.bid.clone())
.filter_map(update_into_message_target)
.filter_map_async(find_or_create_db_user_from_update)
.branch(my_listings_inline_handler())
.branch(
@@ -156,20 +154,21 @@ async fn main() -> Result<()> {
// Set up the bot's command menu
setup_bot_commands(&bot).await?;
let handler_with_deps = dptree::entry()
.filter_map(|bot: Box<Bot>, update: Update, daos: DAOs| {
let target = update_into_message_target(update)?;
Some(App::new(
Box::new(BotMessageSender::new(*bot, target)),
daos.clone(),
))
})
.chain(main_handler());
let dialog_storage = SqliteStorage::new(db_pool.clone(), Json).await?;
let daos = DAOs::new(db_pool.clone());
let app = App::new(bot.clone(), daos.clone());
// Create dispatcher with dialogue system
Dispatcher::builder(bot, main_handler())
.dependencies(dptree::deps![
dialog_storage,
daos,
app.daos.user.clone(),
app.daos.listing.clone(),
app.daos.bid.clone(),
app
])
Dispatcher::builder(bot, handler_with_deps)
.dependencies(dptree::deps![dialog_storage, daos])
.enable_ctrlc_handler()
.worker_queue_size(1)
.build()
@@ -199,7 +198,7 @@ mod tests {
let mut bot = MockMessageSender::new();
bot.expect_send_html_message()
.times(1)
.returning(|_, _, _| Ok(()));
.returning(|_, _| Ok(()));
let deps = create_deps(bot).await;
let handler = main_handler();
dptree::type_check(handler.sig(), &deps, &[]);

View File

@@ -14,10 +14,10 @@ use teloxide::{
pub trait MessageSender {
async fn send_html_message(
&self,
target: MessageTarget,
text: String,
keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult;
fn with_target(&self, target: MessageTarget) -> BoxedMessageSender;
async fn answer_inline_query(
&self,
inline_query_id: InlineQueryId,
@@ -27,7 +27,7 @@ pub trait MessageSender {
async fn get_me(&self) -> BotResult<Me>;
}
pub type BoxMessageSender = Box<dyn MessageSender + Send + Sync>;
pub type BoxedMessageSender = Box<dyn MessageSender + Send + Sync>;
#[cfg(test)]
mockall::mock! {
@@ -37,9 +37,9 @@ mockall::mock! {
}
#[async_trait]
impl MessageSender for MessageSender {
fn with_target(&self, target: MessageTarget) -> BoxedMessageSender;
async fn send_html_message(
&self,
target: MessageTarget,
text: String,
keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult;
@@ -53,17 +53,29 @@ mockall::mock! {
}
}
pub struct BotMessageSender(Bot, MessageTarget);
impl BotMessageSender {
pub fn new(bot: Bot, message_target: MessageTarget) -> Self {
Self(bot, message_target)
}
}
#[async_trait]
impl MessageSender for Bot {
impl MessageSender for BotMessageSender {
fn with_target(&self, target: MessageTarget) -> BoxedMessageSender {
let clone = Self(self.0.clone(), target);
Box::new(clone)
}
async fn send_html_message(
&self,
target: MessageTarget,
text: String,
keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult {
let target = self.1.clone();
if let Some(message_id) = target.message_id {
log::info!("Editing message in chat: {target:?}");
let mut message = self
.0
.edit_message_text(target.chat_id, message_id, &text)
.parse_mode(ParseMode::Html);
if let Some(kb) = keyboard {
@@ -73,6 +85,7 @@ impl MessageSender for Bot {
} else {
log::info!("Sending message to chat: {target:?}");
let mut message = self
.0
.send_message(target.chat_id, &text)
.parse_mode(ParseMode::Html);
if let Some(kb) = keyboard {
@@ -88,21 +101,24 @@ impl MessageSender for Bot {
inline_query_id: InlineQueryId,
results: Vec<InlineQueryResult>,
) -> BotResult {
teloxide::prelude::Requester::answer_inline_query(self, inline_query_id, results)
self.0
.answer_inline_query(inline_query_id, results)
.await
.map(|_| ())
.map_err(|err| BotError::InternalError(err.into()))
}
async fn answer_callback_query(&self, query_id: CallbackQueryId) -> BotResult {
teloxide::prelude::Requester::answer_callback_query(self, query_id)
self.0
.answer_callback_query(query_id)
.await
.map(|_| ())
.map_err(|err| BotError::InternalError(err.into()))
}
async fn get_me(&self) -> BotResult<Me> {
teloxide::prelude::Requester::get_me(self)
self.0
.get_me()
.await
.map_err(|err| BotError::InternalError(err.into()))
}

View File

@@ -1,4 +1,4 @@
use crate::{message_sender::BoxMessageSender, BotResult};
use crate::{message_sender::BoxedMessageSender, BotResult};
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use num::One;
@@ -127,7 +127,7 @@ pub fn create_single_button_keyboard(text: &str, callback_data: &str) -> InlineK
// Extract callback data and answer callback query
pub async fn extract_callback_data(
bot: &BoxMessageSender,
bot: &BoxedMessageSender,
callback_query: CallbackQuery,
) -> BotResult<String> {
let data = match callback_query.data {

View File

@@ -178,7 +178,8 @@ pub async fn create_deps(mock_bot: MockMessageSender) -> DependencyMap {
let update = create_tele_update();
let pool = create_test_pool().await;
let dialog_storage = SqliteStorage::new(pool.clone(), Json).await.unwrap();
let app = App::new(Box::new(mock_bot), DAOs::new(pool));
let daos = DAOs::new(pool);
let app = App::new(Box::new(mock_bot), daos.clone());
let me_user = create_tele_user("me");
let me = Me {
user: me_user,
@@ -188,7 +189,7 @@ pub async fn create_deps(mock_bot: MockMessageSender) -> DependencyMap {
can_connect_to_business: true,
has_main_web_app: true,
};
dptree::deps![update, dialog_storage, app, me]
dptree::deps![update, dialog_storage, app, me, daos]
}
#[cfg(test)]