Refactor bot commands and database models
- Update my_listings command structure and keyboard handling - Enhance new_listing workflow with improved callbacks and handlers - Refactor database user model and keyboard utilities - Add new handler utilities module - Update main bot configuration and start command
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1618,6 +1618,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"num",
|
||||
"regex",
|
||||
"rstest",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
|
||||
@@ -27,6 +27,7 @@ teloxide-core = "0.13.0"
|
||||
num = "0.4.3"
|
||||
itertools = "0.14.0"
|
||||
async-trait = "0.1"
|
||||
regex = "1.11.2"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.26.1"
|
||||
|
||||
@@ -1,8 +1,62 @@
|
||||
use crate::keyboard_buttons;
|
||||
use crate::{
|
||||
db::{listing::PersistedListing, ListingDbId},
|
||||
keyboard_buttons,
|
||||
};
|
||||
use regex::Regex;
|
||||
use teloxide::types::InlineKeyboardButton;
|
||||
|
||||
keyboard_buttons! {
|
||||
pub enum MyListingsButtons {
|
||||
BackToMenu("⬅️ Back to Menu", "my_listings_back_to_menu"),
|
||||
// keyboard_buttons! {
|
||||
// pub enum MyListingsButtons {
|
||||
// // SelectListing("Select Listing", "my_listings:", ListingDbId ),
|
||||
// SelectListing("Select Listing", "my_listings:"),
|
||||
// BackToMenu("⬅️ Back to Menu", "my_listings_back_to_menu"),
|
||||
// }
|
||||
// }
|
||||
|
||||
pub enum MyListingsButtons {
|
||||
SelectListing(ListingDbId),
|
||||
NewListing,
|
||||
BackToMenu,
|
||||
}
|
||||
impl MyListingsButtons {
|
||||
pub fn listing_into_button(listing: &PersistedListing) -> InlineKeyboardButton {
|
||||
InlineKeyboardButton::callback(
|
||||
&listing.base.title,
|
||||
Self::encode_listing_id(listing.persisted.id),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn back_to_menu_into_button() -> InlineKeyboardButton {
|
||||
InlineKeyboardButton::callback("Back to Menu", "my_listings_back_to_menu")
|
||||
}
|
||||
|
||||
pub fn new_listing_into_button() -> InlineKeyboardButton {
|
||||
InlineKeyboardButton::callback("🛍️ New Listing", "my_listings_new_listing")
|
||||
}
|
||||
|
||||
fn encode_listing_id(listing_id: ListingDbId) -> String {
|
||||
format!("my_listings:{listing_id}")
|
||||
}
|
||||
|
||||
fn decode_listing_id(value: &str) -> Option<ListingDbId> {
|
||||
let re = Regex::new(r"my_listings:(\d+)").ok()?;
|
||||
let caps = re.captures(value)?;
|
||||
let listing_id = caps.get(1)?.as_str().parse::<i64>().ok()?;
|
||||
Some(ListingDbId::new(listing_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for MyListingsButtons {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
if let Some(listing_id) = Self::decode_listing_id(value) {
|
||||
return Ok(MyListingsButtons::SelectListing(listing_id));
|
||||
}
|
||||
match value {
|
||||
"my_listings_new_listing" => Ok(MyListingsButtons::NewListing),
|
||||
"my_listings_back_to_menu" => Ok(MyListingsButtons::BackToMenu),
|
||||
_ => anyhow::bail!("Unknown MyListingsButtons: {value}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,15 @@ use crate::{
|
||||
commands::{
|
||||
enter_main_menu,
|
||||
my_listings::keyboard::{ManageListingButtons, MyListingsButtons},
|
||||
new_listing::{enter_edit_listing_draft, ListingDraft},
|
||||
new_listing::{enter_edit_listing_draft, enter_select_new_listing_type, ListingDraft},
|
||||
},
|
||||
db::{
|
||||
listing::{ListingFields, PersistedListing},
|
||||
user::PersistedUser,
|
||||
ListingDAO, ListingDbId, UserDAO,
|
||||
ListingDAO, ListingDbId,
|
||||
},
|
||||
handler_utils::{
|
||||
find_or_create_db_user_from_callback_query, find_or_create_db_user_from_message,
|
||||
},
|
||||
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
|
||||
Command, DialogueRootState, HandlerResult, RootDialogue,
|
||||
@@ -41,7 +44,6 @@ impl From<MyListingsState> for DialogueRootState {
|
||||
|
||||
pub fn my_listings_inline_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
||||
Update::filter_inline_query()
|
||||
.inspect(|query: InlineQuery| info!("Received inline query: {:?}", query))
|
||||
.filter_map_async(inline_query_extract_forward_listing)
|
||||
.endpoint(handle_forward_listing)
|
||||
}
|
||||
@@ -50,7 +52,9 @@ pub fn my_listings_handler() -> Handler<'static, HandlerResult, DpHandlerDescrip
|
||||
dptree::entry()
|
||||
.branch(
|
||||
Update::filter_message().filter_command::<Command>().branch(
|
||||
dptree::case![Command::MyListings].endpoint(handle_my_listings_command_input),
|
||||
dptree::case![Command::MyListings]
|
||||
.filter_map_async(find_or_create_db_user_from_message)
|
||||
.endpoint(handle_my_listings_command_input),
|
||||
),
|
||||
)
|
||||
.branch(
|
||||
@@ -60,12 +64,14 @@ pub fn my_listings_handler() -> Handler<'static, HandlerResult, DpHandlerDescrip
|
||||
case![DialogueRootState::MyListings(
|
||||
MyListingsState::ViewingListings
|
||||
)]
|
||||
.filter_map_async(find_or_create_db_user_from_callback_query)
|
||||
.endpoint(handle_viewing_listings_callback),
|
||||
)
|
||||
.branch(
|
||||
case![DialogueRootState::MyListings(
|
||||
MyListingsState::ManagingListing(listing_id)
|
||||
)]
|
||||
.filter_map_async(find_or_create_db_user_from_callback_query)
|
||||
.endpoint(handle_managing_listing_callback),
|
||||
),
|
||||
)
|
||||
@@ -76,7 +82,7 @@ async fn inline_query_extract_forward_listing(
|
||||
inline_query: InlineQuery,
|
||||
) -> Option<PersistedListing> {
|
||||
let query = &inline_query.query;
|
||||
info!("Try to extract forward listing from query: {}", query);
|
||||
info!("Try to extract forward listing from query: {query}");
|
||||
let listing_id_str = query.split("forward_listing:").nth(1)?;
|
||||
let listing_id = ListingDbId::new(listing_id_str.parse::<i64>().ok()?);
|
||||
let listing = ListingDAO::find_by_id(&db_pool, listing_id)
|
||||
@@ -90,10 +96,7 @@ async fn handle_forward_listing(
|
||||
inline_query: InlineQuery,
|
||||
listing: PersistedListing,
|
||||
) -> HandlerResult {
|
||||
info!(
|
||||
"Handling forward listing inline query for listing {:?}",
|
||||
listing
|
||||
);
|
||||
info!("Handling forward listing inline query for listing {listing:?}");
|
||||
|
||||
let bot_username = match bot.get_me().await?.username.as_ref() {
|
||||
Some(username) => username.to_string(),
|
||||
@@ -188,68 +191,46 @@ async fn handle_my_listings_command_input(
|
||||
db_pool: SqlitePool,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
user: PersistedUser,
|
||||
msg: Message,
|
||||
) -> HandlerResult {
|
||||
let from = msg.from.unwrap();
|
||||
show_listings_for_user(db_pool, dialogue, bot, from.id, msg.chat).await?;
|
||||
enter_my_listings(db_pool, bot, dialogue, user, msg.chat).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn show_listings_for_user(
|
||||
pub async fn enter_my_listings(
|
||||
db_pool: SqlitePool,
|
||||
dialogue: RootDialogue,
|
||||
bot: Bot,
|
||||
user: teloxide::types::UserId,
|
||||
dialogue: RootDialogue,
|
||||
user: PersistedUser,
|
||||
target: impl Into<MessageTarget>,
|
||||
) -> HandlerResult {
|
||||
// If we reach here, show the listings menu
|
||||
let user = match UserDAO::find_by_telegram_id(&db_pool, user).await? {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
send_message(
|
||||
&bot,
|
||||
target,
|
||||
"You don't have an account. Try creating an auction first.",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
return Err(anyhow::anyhow!("User not found"));
|
||||
}
|
||||
};
|
||||
|
||||
// Transition to ViewingListings state
|
||||
dialogue.update(MyListingsState::ViewingListings).await?;
|
||||
|
||||
let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?;
|
||||
if listings.is_empty() {
|
||||
// Create keyboard with just the back button
|
||||
let keyboard =
|
||||
teloxide::types::InlineKeyboardMarkup::new([[MyListingsButtons::BackToMenu.into()]]);
|
||||
// Create keyboard with buttons for each listing
|
||||
let mut keyboard = teloxide::types::InlineKeyboardMarkup::default();
|
||||
for listing in &listings {
|
||||
keyboard = keyboard.append_row(vec![MyListingsButtons::listing_into_button(listing)]);
|
||||
}
|
||||
keyboard = keyboard.append_row(vec![
|
||||
MyListingsButtons::new_listing_into_button(),
|
||||
MyListingsButtons::back_to_menu_into_button(),
|
||||
]);
|
||||
|
||||
if listings.is_empty() {
|
||||
send_message(
|
||||
&bot,
|
||||
target,
|
||||
"📋 <b>My Listings</b>\n\n\
|
||||
You don't have any listings yet.\n\
|
||||
Use /newlisting to create your first listing!",
|
||||
You don't have any listings yet.",
|
||||
Some(keyboard),
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create keyboard with buttons for each listing
|
||||
let mut keyboard = teloxide::types::InlineKeyboardMarkup::default();
|
||||
for listing in &listings {
|
||||
keyboard = keyboard.append_row(vec![InlineKeyboardButton::callback(
|
||||
listing.base.title.to_string(),
|
||||
listing.persisted.id.to_string(),
|
||||
)]);
|
||||
}
|
||||
|
||||
// Add back to menu button
|
||||
keyboard = keyboard.append_row(vec![MyListingsButtons::BackToMenu.into()]);
|
||||
|
||||
let response = format!(
|
||||
"📋 <b>My Listings</b>\n\n\
|
||||
You have {}.\n\n\
|
||||
@@ -266,30 +247,32 @@ async fn handle_viewing_listings_callback(
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
callback_query: CallbackQuery,
|
||||
user: PersistedUser,
|
||||
) -> HandlerResult {
|
||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||
let target = (from.clone(), message_id);
|
||||
|
||||
// Check if it's the back to menu button
|
||||
if let Ok(button) = MyListingsButtons::try_from(data.as_str()) {
|
||||
match button {
|
||||
MyListingsButtons::BackToMenu => {
|
||||
// Transition back to main menu using the reusable function
|
||||
enter_main_menu(bot, dialogue, target).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let button = MyListingsButtons::try_from(data.as_str())?;
|
||||
|
||||
match button {
|
||||
MyListingsButtons::SelectListing(listing_id) => {
|
||||
let listing =
|
||||
get_listing_for_user(&db_pool, &bot, user, listing_id, target.clone()).await?;
|
||||
dialogue
|
||||
.update(MyListingsState::ManagingListing(listing_id))
|
||||
.await?;
|
||||
show_listing_details(&bot, listing, target).await?;
|
||||
}
|
||||
MyListingsButtons::NewListing => {
|
||||
enter_select_new_listing_type(bot, dialogue, target).await?;
|
||||
}
|
||||
MyListingsButtons::BackToMenu => {
|
||||
// Transition back to main menu using the reusable function
|
||||
enter_main_menu(bot, dialogue, target).await?
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, treat it as a listing ID
|
||||
let listing_id = ListingDbId::new(data.parse::<i64>()?);
|
||||
let (_, listing) =
|
||||
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
||||
dialogue
|
||||
.update(MyListingsState::ManagingListing(listing_id))
|
||||
.await?;
|
||||
show_listing_details(&bot, listing, target).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -339,6 +322,7 @@ async fn handle_managing_listing_callback(
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
callback_query: CallbackQuery,
|
||||
user: PersistedUser,
|
||||
listing_id: ListingDbId,
|
||||
) -> HandlerResult {
|
||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||
@@ -346,16 +330,17 @@ async fn handle_managing_listing_callback(
|
||||
|
||||
match ManageListingButtons::try_from(data.as_str())? {
|
||||
ManageListingButtons::PreviewMessage => {
|
||||
let (_, listing) =
|
||||
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
||||
let listing = ListingDAO::find_by_id(&db_pool, listing_id)
|
||||
.await?
|
||||
.ok_or(anyhow::anyhow!("Listing not found"))?;
|
||||
send_preview_listing_message(&bot, listing, from).await?;
|
||||
}
|
||||
ManageListingButtons::ForwardListing => {
|
||||
unimplemented!("Forward listing not implemented");
|
||||
}
|
||||
ManageListingButtons::Edit => {
|
||||
let (_, listing) =
|
||||
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
||||
let listing =
|
||||
get_listing_for_user(&db_pool, &bot, user, listing_id, target.clone()).await?;
|
||||
let draft = ListingDraft::from_persisted(listing);
|
||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
||||
}
|
||||
@@ -365,7 +350,7 @@ async fn handle_managing_listing_callback(
|
||||
}
|
||||
ManageListingButtons::Back => {
|
||||
dialogue.update(MyListingsState::ViewingListings).await?;
|
||||
show_listings_for_user(db_pool, dialogue, bot, from.id, target).await?;
|
||||
enter_my_listings(db_pool, bot, dialogue, user, target).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,7 +396,7 @@ async fn send_preview_listing_message(
|
||||
let mut response_lines = vec![];
|
||||
response_lines.push(format!("<b>{}</b>", &listing.base.title));
|
||||
if let Some(description) = &listing.base.description {
|
||||
response_lines.push(format!("{}", description));
|
||||
response_lines.push(description.to_owned());
|
||||
}
|
||||
send_message(
|
||||
bot,
|
||||
@@ -423,27 +408,13 @@ async fn send_preview_listing_message(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_and_listing(
|
||||
async fn get_listing_for_user(
|
||||
db_pool: &SqlitePool,
|
||||
bot: &Bot,
|
||||
user_id: teloxide::types::UserId,
|
||||
user: PersistedUser,
|
||||
listing_id: ListingDbId,
|
||||
target: impl Into<MessageTarget>,
|
||||
) -> HandlerResult<(PersistedUser, PersistedListing)> {
|
||||
let user = match UserDAO::find_by_telegram_id(db_pool, user_id).await? {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
send_message(
|
||||
bot,
|
||||
target,
|
||||
"❌ You don't have an account. Try creating an auction first.",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
return Err(anyhow::anyhow!("User not found"));
|
||||
}
|
||||
};
|
||||
|
||||
) -> HandlerResult<PersistedListing> {
|
||||
let listing = match ListingDAO::find_by_id(db_pool, listing_id).await? {
|
||||
Some(listing) => listing,
|
||||
None => {
|
||||
@@ -463,5 +434,5 @@ async fn get_user_and_listing(
|
||||
return Err(anyhow::anyhow!("User does not own listing"));
|
||||
}
|
||||
|
||||
Ok((user, listing))
|
||||
Ok(listing)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use crate::{
|
||||
commands::{
|
||||
new_listing::{
|
||||
enter_select_new_listing_type,
|
||||
field_processing::transition_to_field,
|
||||
keyboard::{
|
||||
DurationKeyboardButtons, ListingTypeKeyboardButtons, SlotsKeyboardButtons,
|
||||
@@ -17,7 +18,7 @@ use crate::{
|
||||
},
|
||||
start::enter_main_menu,
|
||||
},
|
||||
db::{listing::ListingFields, ListingDuration, ListingType, UserDbId},
|
||||
db::{listing::ListingFields, user::PersistedUser, ListingDuration, ListingType},
|
||||
message_utils::*,
|
||||
HandlerResult, RootDialogue,
|
||||
};
|
||||
@@ -28,7 +29,7 @@ use teloxide::{types::CallbackQuery, Bot};
|
||||
pub async fn handle_selecting_listing_type_callback(
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
seller_id: UserDbId,
|
||||
user: PersistedUser,
|
||||
callback_query: CallbackQuery,
|
||||
) -> HandlerResult {
|
||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||
@@ -52,7 +53,7 @@ pub async fn handle_selecting_listing_type_callback(
|
||||
};
|
||||
|
||||
// Create draft with selected listing type
|
||||
let draft = ListingDraft::new_for_seller_with_type(seller_id, listing_type);
|
||||
let draft = ListingDraft::new_for_seller_with_type(user.persisted.id, listing_type);
|
||||
|
||||
// Transition to first field (Title)
|
||||
transition_to_field(dialogue, ListingField::Title, draft).await?;
|
||||
@@ -86,8 +87,12 @@ pub async fn handle_awaiting_draft_field_callback(
|
||||
info!("User {from:?} selected callback: {data:?}");
|
||||
let target = (from, message_id);
|
||||
|
||||
if let Ok(ListingTypeKeyboardButtons::Back) = data.as_str().try_into() {
|
||||
return enter_select_new_listing_type(bot, dialogue, target).await;
|
||||
}
|
||||
|
||||
if data == "cancel" {
|
||||
return cancel_wizard(&bot, dialogue, target).await;
|
||||
return cancel_wizard(bot, dialogue, target).await;
|
||||
}
|
||||
|
||||
// Unified callback dispatch
|
||||
@@ -248,13 +253,12 @@ async fn handle_duration_callback(
|
||||
|
||||
/// Cancel the wizard and exit
|
||||
pub async fn cancel_wizard(
|
||||
bot: &Bot,
|
||||
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?;
|
||||
enter_select_new_listing_type(bot, dialogue, target).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::{callbacks::*, handlers::*, types::*};
|
||||
use crate::{case, Command, DialogueRootState, Handler};
|
||||
use crate::{
|
||||
case, handler_utils::find_or_create_db_user_from_callback_query, Command, DialogueRootState,
|
||||
Handler,
|
||||
};
|
||||
use teloxide::{dptree, prelude::*, types::Update};
|
||||
|
||||
// Create the dialogue handler tree for new listing wizard
|
||||
@@ -30,8 +33,9 @@ pub fn new_listing_handler() -> Handler {
|
||||
Update::filter_callback_query()
|
||||
.branch(
|
||||
case![DialogueRootState::NewListing(
|
||||
NewListingState::SelectingListingType { seller_id }
|
||||
NewListingState::SelectingListingType
|
||||
)]
|
||||
.filter_map_async(find_or_create_db_user_from_callback_query)
|
||||
.endpoint(handle_selecting_listing_type_callback),
|
||||
)
|
||||
.branch(
|
||||
|
||||
@@ -20,41 +20,33 @@ use crate::{
|
||||
},
|
||||
db::{
|
||||
listing::{ListingFields, NewListing, PersistedListing},
|
||||
ListingDAO, UserDAO,
|
||||
ListingDAO,
|
||||
},
|
||||
message_utils::*,
|
||||
DialogueRootState, HandlerResult, RootDialogue,
|
||||
};
|
||||
use log::{error, info};
|
||||
use log::info;
|
||||
use sqlx::SqlitePool;
|
||||
use teloxide::{prelude::*, types::*, Bot};
|
||||
|
||||
/// Handle the /newlisting command - starts the dialogue
|
||||
pub(super) async fn handle_new_listing_command(
|
||||
db_pool: SqlitePool,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
msg: Message,
|
||||
) -> HandlerResult {
|
||||
let user = msg.from.ok_or_else(|| anyhow::anyhow!("User not found"))?;
|
||||
enter_handle_new_listing(db_pool, bot, dialogue, user, msg.chat).await?;
|
||||
enter_select_new_listing_type(bot, dialogue, msg.chat).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn enter_handle_new_listing(
|
||||
db_pool: SqlitePool,
|
||||
pub async fn enter_select_new_listing_type(
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
user: User,
|
||||
target: impl Into<MessageTarget>,
|
||||
) -> HandlerResult {
|
||||
let user = UserDAO::find_or_create_by_telegram_user(&db_pool, user).await?;
|
||||
|
||||
// Initialize the dialogue to listing type selection state
|
||||
dialogue
|
||||
.update(NewListingState::SelectingListingType {
|
||||
seller_id: user.persisted.id,
|
||||
})
|
||||
.update(NewListingState::SelectingListingType)
|
||||
.await?;
|
||||
|
||||
send_message(
|
||||
@@ -84,7 +76,7 @@ pub async fn handle_awaiting_draft_field_input(
|
||||
);
|
||||
|
||||
if is_cancel(text) {
|
||||
return cancel_wizard(&bot, dialogue, chat).await;
|
||||
return cancel_wizard(bot, dialogue, chat).await;
|
||||
}
|
||||
|
||||
// Process the field update
|
||||
@@ -145,14 +137,6 @@ pub async fn handle_viewing_draft_callback(
|
||||
callback_query: CallbackQuery,
|
||||
) -> HandlerResult {
|
||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||
|
||||
// Ensure the user exists before saving the listing
|
||||
UserDAO::find_or_create_by_telegram_user(&db_pool, from.clone())
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!("Error finding or creating user: {e}");
|
||||
})?;
|
||||
|
||||
let target = (from.clone(), message_id);
|
||||
|
||||
match ConfirmationKeyboardButtons::try_from(data.as_str())? {
|
||||
|
||||
@@ -54,7 +54,10 @@ pub fn get_edit_success_message(field: ListingField) -> &'static str {
|
||||
/// Get the appropriate keyboard for a field
|
||||
pub fn get_keyboard_for_field(field: ListingField) -> Option<InlineKeyboardMarkup> {
|
||||
match field {
|
||||
ListingField::Title => Some(create_cancel_keyboard()),
|
||||
ListingField::Title => Some(InlineKeyboardMarkup::new([[
|
||||
// Back to listing type selection
|
||||
ListingTypeKeyboardButtons::Back.to_button(),
|
||||
]])),
|
||||
ListingField::Description => Some(create_skip_cancel_keyboard()),
|
||||
ListingField::Price => None,
|
||||
ListingField::Slots => Some(SlotsKeyboardButtons::to_keyboard()),
|
||||
@@ -63,11 +66,6 @@ pub fn get_keyboard_for_field(field: ListingField) -> Option<InlineKeyboardMarku
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard creation helpers
|
||||
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")]])
|
||||
}
|
||||
|
||||
@@ -20,12 +20,11 @@ mod keyboard;
|
||||
pub mod messages;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod types;
|
||||
mod ui;
|
||||
mod validations;
|
||||
|
||||
// Re-export the main handler for external use
|
||||
pub use handler_factory::new_listing_handler;
|
||||
pub use handlers::{enter_edit_listing_draft, enter_handle_new_listing};
|
||||
pub use handlers::{enter_edit_listing_draft, enter_select_new_listing_type};
|
||||
pub use types::*;
|
||||
|
||||
@@ -89,9 +89,7 @@ pub enum ListingField {
|
||||
// Dialogue state for the new listing wizard
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum NewListingState {
|
||||
SelectingListingType {
|
||||
seller_id: UserDbId,
|
||||
},
|
||||
SelectingListingType,
|
||||
AwaitingDraftField {
|
||||
field: ListingField,
|
||||
draft: ListingDraft,
|
||||
|
||||
@@ -8,7 +8,8 @@ use teloxide::{
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{
|
||||
commands::{my_listings::show_listings_for_user, new_listing::enter_handle_new_listing},
|
||||
commands::my_listings::enter_my_listings,
|
||||
db::user::PersistedUser,
|
||||
keyboard_buttons,
|
||||
message_utils::{extract_callback_data, send_message, MessageTarget},
|
||||
Command, DialogueRootState, HandlerResult, RootDialogue,
|
||||
@@ -16,9 +17,6 @@ use crate::{
|
||||
|
||||
keyboard_buttons! {
|
||||
pub enum MainMenuButtons {
|
||||
[
|
||||
NewListing("🛍️ New Listing", "menu_new_listing"),
|
||||
],
|
||||
[
|
||||
MyListings("📋 My Listings", "menu_my_listings"),
|
||||
MyBids("💰 My Bids", "menu_my_bids"),
|
||||
@@ -69,6 +67,7 @@ pub async fn handle_main_menu_callback(
|
||||
db_pool: SqlitePool,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
user: PersistedUser,
|
||||
callback_query: CallbackQuery,
|
||||
) -> HandlerResult {
|
||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||
@@ -82,12 +81,9 @@ pub async fn handle_main_menu_callback(
|
||||
|
||||
let button = MainMenuButtons::try_from(data.as_str())?;
|
||||
match button {
|
||||
MainMenuButtons::NewListing => {
|
||||
enter_handle_new_listing(db_pool, bot, dialogue, from.clone(), target).await?;
|
||||
}
|
||||
MainMenuButtons::MyListings => {
|
||||
// Call show_listings_for_user directly
|
||||
show_listings_for_user(db_pool, dialogue, bot, from.id, target).await?;
|
||||
enter_my_listings(db_pool, bot, dialogue, user, target).await?;
|
||||
}
|
||||
MainMenuButtons::MyBids => {
|
||||
send_message(
|
||||
|
||||
@@ -8,7 +8,7 @@ pub type PersistedUser = User<PersistedUserFields>;
|
||||
pub type NewUser = User<()>;
|
||||
|
||||
/// Core user information
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
#[derive(Clone, FromRow)]
|
||||
#[allow(unused)]
|
||||
pub struct User<P: Debug + Clone> {
|
||||
pub persisted: P,
|
||||
@@ -19,6 +19,22 @@ pub struct User<P: Debug + Clone> {
|
||||
pub is_banned: bool,
|
||||
}
|
||||
|
||||
impl Debug for User<PersistedUserFields> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let name = if let Some(last_name) = self.last_name.as_deref() {
|
||||
format!("{} {}", self.first_name, last_name)
|
||||
} else {
|
||||
self.first_name.clone()
|
||||
};
|
||||
let username = self.username.as_deref().unwrap_or("");
|
||||
write!(
|
||||
f,
|
||||
"User(id: {} / {}, '{}' @{})",
|
||||
self.persisted.id, self.telegram_id, name, username
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(unused)]
|
||||
pub struct PersistedUserFields {
|
||||
|
||||
36
src/handler_utils.rs
Normal file
36
src/handler_utils.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use sqlx::SqlitePool;
|
||||
use teloxide::types::{CallbackQuery, Message};
|
||||
|
||||
use crate::db::{user::PersistedUser, UserDAO};
|
||||
|
||||
pub async fn find_or_create_db_user_from_message(
|
||||
db_pool: SqlitePool,
|
||||
message: Message,
|
||||
) -> Option<PersistedUser> {
|
||||
let user = message.from?;
|
||||
find_or_create_db_user(db_pool, user).await
|
||||
}
|
||||
|
||||
pub async fn find_or_create_db_user_from_callback_query(
|
||||
db_pool: SqlitePool,
|
||||
callback_query: CallbackQuery,
|
||||
) -> Option<PersistedUser> {
|
||||
let user = callback_query.from;
|
||||
find_or_create_db_user(db_pool, user).await
|
||||
}
|
||||
|
||||
pub async fn find_or_create_db_user(
|
||||
db_pool: SqlitePool,
|
||||
user: teloxide::types::User,
|
||||
) -> Option<PersistedUser> {
|
||||
match UserDAO::find_or_create_by_telegram_user(&db_pool, user).await {
|
||||
Ok(user) => {
|
||||
log::debug!("loaded user from db: {user:?}");
|
||||
Some(user)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error finding or creating user: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,13 @@ macro_rules! keyboard_buttons {
|
||||
$($($name::$variant => $text),*),*
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn callback_data(self) -> &'static str {
|
||||
match self {
|
||||
$($($name::$variant => $callback_data),*),*
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<$name> for teloxide::types::InlineKeyboardButton {
|
||||
fn from(value: $name) -> Self {
|
||||
|
||||
13
src/main.rs
13
src/main.rs
@@ -2,17 +2,21 @@ mod commands;
|
||||
mod config;
|
||||
mod db;
|
||||
mod dptree_utils;
|
||||
mod handler_utils;
|
||||
mod keyboard_utils;
|
||||
mod message_utils;
|
||||
mod sqlite_storage;
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
|
||||
use crate::commands::{
|
||||
my_listings::{my_listings_handler, my_listings_inline_handler, MyListingsState},
|
||||
new_listing::{new_listing_handler, NewListingState},
|
||||
};
|
||||
use crate::sqlite_storage::SqliteStorage;
|
||||
use crate::{
|
||||
commands::{
|
||||
my_listings::{my_listings_handler, my_listings_inline_handler, MyListingsState},
|
||||
new_listing::{new_listing_handler, NewListingState},
|
||||
},
|
||||
handler_utils::find_or_create_db_user_from_callback_query,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use commands::*;
|
||||
use config::Config;
|
||||
@@ -98,6 +102,7 @@ async fn main() -> Result<()> {
|
||||
.branch(
|
||||
Update::filter_callback_query().branch(
|
||||
dptree::case![DialogueRootState::MainMenu]
|
||||
.filter_map_async(find_or_create_db_user_from_callback_query)
|
||||
.endpoint(handle_main_menu_callback),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user