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",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"num",
|
"num",
|
||||||
|
"regex",
|
||||||
"rstest",
|
"rstest",
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ teloxide-core = "0.13.0"
|
|||||||
num = "0.4.3"
|
num = "0.4.3"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
regex = "1.11.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = "0.26.1"
|
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! {
|
// keyboard_buttons! {
|
||||||
pub enum MyListingsButtons {
|
// pub enum MyListingsButtons {
|
||||||
BackToMenu("⬅️ Back to Menu", "my_listings_back_to_menu"),
|
// // 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::{
|
commands::{
|
||||||
enter_main_menu,
|
enter_main_menu,
|
||||||
my_listings::keyboard::{ManageListingButtons, MyListingsButtons},
|
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::{
|
db::{
|
||||||
listing::{ListingFields, PersistedListing},
|
listing::{ListingFields, PersistedListing},
|
||||||
user::PersistedUser,
|
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},
|
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
|
||||||
Command, DialogueRootState, HandlerResult, RootDialogue,
|
Command, DialogueRootState, HandlerResult, RootDialogue,
|
||||||
@@ -41,7 +44,6 @@ impl From<MyListingsState> for DialogueRootState {
|
|||||||
|
|
||||||
pub fn my_listings_inline_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
pub fn my_listings_inline_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> {
|
||||||
Update::filter_inline_query()
|
Update::filter_inline_query()
|
||||||
.inspect(|query: InlineQuery| info!("Received inline query: {:?}", query))
|
|
||||||
.filter_map_async(inline_query_extract_forward_listing)
|
.filter_map_async(inline_query_extract_forward_listing)
|
||||||
.endpoint(handle_forward_listing)
|
.endpoint(handle_forward_listing)
|
||||||
}
|
}
|
||||||
@@ -50,7 +52,9 @@ pub fn my_listings_handler() -> Handler<'static, HandlerResult, DpHandlerDescrip
|
|||||||
dptree::entry()
|
dptree::entry()
|
||||||
.branch(
|
.branch(
|
||||||
Update::filter_message().filter_command::<Command>().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(
|
.branch(
|
||||||
@@ -60,12 +64,14 @@ pub fn my_listings_handler() -> Handler<'static, HandlerResult, DpHandlerDescrip
|
|||||||
case![DialogueRootState::MyListings(
|
case![DialogueRootState::MyListings(
|
||||||
MyListingsState::ViewingListings
|
MyListingsState::ViewingListings
|
||||||
)]
|
)]
|
||||||
|
.filter_map_async(find_or_create_db_user_from_callback_query)
|
||||||
.endpoint(handle_viewing_listings_callback),
|
.endpoint(handle_viewing_listings_callback),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::MyListings(
|
case![DialogueRootState::MyListings(
|
||||||
MyListingsState::ManagingListing(listing_id)
|
MyListingsState::ManagingListing(listing_id)
|
||||||
)]
|
)]
|
||||||
|
.filter_map_async(find_or_create_db_user_from_callback_query)
|
||||||
.endpoint(handle_managing_listing_callback),
|
.endpoint(handle_managing_listing_callback),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -76,7 +82,7 @@ async fn inline_query_extract_forward_listing(
|
|||||||
inline_query: InlineQuery,
|
inline_query: InlineQuery,
|
||||||
) -> Option<PersistedListing> {
|
) -> Option<PersistedListing> {
|
||||||
let query = &inline_query.query;
|
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_str = query.split("forward_listing:").nth(1)?;
|
||||||
let listing_id = ListingDbId::new(listing_id_str.parse::<i64>().ok()?);
|
let listing_id = ListingDbId::new(listing_id_str.parse::<i64>().ok()?);
|
||||||
let listing = ListingDAO::find_by_id(&db_pool, listing_id)
|
let listing = ListingDAO::find_by_id(&db_pool, listing_id)
|
||||||
@@ -90,10 +96,7 @@ async fn handle_forward_listing(
|
|||||||
inline_query: InlineQuery,
|
inline_query: InlineQuery,
|
||||||
listing: PersistedListing,
|
listing: PersistedListing,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
info!(
|
info!("Handling forward listing inline query for listing {listing:?}");
|
||||||
"Handling forward listing inline query for listing {:?}",
|
|
||||||
listing
|
|
||||||
);
|
|
||||||
|
|
||||||
let bot_username = match bot.get_me().await?.username.as_ref() {
|
let bot_username = match bot.get_me().await?.username.as_ref() {
|
||||||
Some(username) => username.to_string(),
|
Some(username) => username.to_string(),
|
||||||
@@ -188,68 +191,46 @@ async fn handle_my_listings_command_input(
|
|||||||
db_pool: SqlitePool,
|
db_pool: SqlitePool,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
|
user: PersistedUser,
|
||||||
msg: Message,
|
msg: Message,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let from = msg.from.unwrap();
|
enter_my_listings(db_pool, bot, dialogue, user, msg.chat).await?;
|
||||||
show_listings_for_user(db_pool, dialogue, bot, from.id, msg.chat).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn show_listings_for_user(
|
pub async fn enter_my_listings(
|
||||||
db_pool: SqlitePool,
|
db_pool: SqlitePool,
|
||||||
dialogue: RootDialogue,
|
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
user: teloxide::types::UserId,
|
dialogue: RootDialogue,
|
||||||
|
user: PersistedUser,
|
||||||
target: impl Into<MessageTarget>,
|
target: impl Into<MessageTarget>,
|
||||||
) -> HandlerResult {
|
) -> 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
|
// Transition to ViewingListings state
|
||||||
dialogue.update(MyListingsState::ViewingListings).await?;
|
dialogue.update(MyListingsState::ViewingListings).await?;
|
||||||
|
|
||||||
let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?;
|
let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?;
|
||||||
if listings.is_empty() {
|
// Create keyboard with buttons for each listing
|
||||||
// Create keyboard with just the back button
|
let mut keyboard = teloxide::types::InlineKeyboardMarkup::default();
|
||||||
let keyboard =
|
for listing in &listings {
|
||||||
teloxide::types::InlineKeyboardMarkup::new([[MyListingsButtons::BackToMenu.into()]]);
|
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(
|
send_message(
|
||||||
&bot,
|
&bot,
|
||||||
target,
|
target,
|
||||||
"📋 <b>My Listings</b>\n\n\
|
"📋 <b>My Listings</b>\n\n\
|
||||||
You don't have any listings yet.\n\
|
You don't have any listings yet.",
|
||||||
Use /newlisting to create your first listing!",
|
|
||||||
Some(keyboard),
|
Some(keyboard),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
return Ok(());
|
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!(
|
let response = format!(
|
||||||
"📋 <b>My Listings</b>\n\n\
|
"📋 <b>My Listings</b>\n\n\
|
||||||
You have {}.\n\n\
|
You have {}.\n\n\
|
||||||
@@ -266,30 +247,32 @@ async fn handle_viewing_listings_callback(
|
|||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
|
user: PersistedUser,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||||
let target = (from.clone(), message_id);
|
let target = (from.clone(), message_id);
|
||||||
|
|
||||||
// Check if it's the back to menu button
|
// Check if it's the back to menu button
|
||||||
if let Ok(button) = MyListingsButtons::try_from(data.as_str()) {
|
let button = MyListingsButtons::try_from(data.as_str())?;
|
||||||
match button {
|
|
||||||
MyListingsButtons::BackToMenu => {
|
match button {
|
||||||
// Transition back to main menu using the reusable function
|
MyListingsButtons::SelectListing(listing_id) => {
|
||||||
enter_main_menu(bot, dialogue, target).await?;
|
let listing =
|
||||||
return Ok(());
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +322,7 @@ async fn handle_managing_listing_callback(
|
|||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
|
user: PersistedUser,
|
||||||
listing_id: ListingDbId,
|
listing_id: ListingDbId,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
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())? {
|
match ManageListingButtons::try_from(data.as_str())? {
|
||||||
ManageListingButtons::PreviewMessage => {
|
ManageListingButtons::PreviewMessage => {
|
||||||
let (_, listing) =
|
let listing = ListingDAO::find_by_id(&db_pool, listing_id)
|
||||||
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
.await?
|
||||||
|
.ok_or(anyhow::anyhow!("Listing not found"))?;
|
||||||
send_preview_listing_message(&bot, listing, from).await?;
|
send_preview_listing_message(&bot, listing, from).await?;
|
||||||
}
|
}
|
||||||
ManageListingButtons::ForwardListing => {
|
ManageListingButtons::ForwardListing => {
|
||||||
unimplemented!("Forward listing not implemented");
|
unimplemented!("Forward listing not implemented");
|
||||||
}
|
}
|
||||||
ManageListingButtons::Edit => {
|
ManageListingButtons::Edit => {
|
||||||
let (_, listing) =
|
let listing =
|
||||||
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
get_listing_for_user(&db_pool, &bot, user, listing_id, target.clone()).await?;
|
||||||
let draft = ListingDraft::from_persisted(listing);
|
let draft = ListingDraft::from_persisted(listing);
|
||||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
||||||
}
|
}
|
||||||
@@ -365,7 +350,7 @@ async fn handle_managing_listing_callback(
|
|||||||
}
|
}
|
||||||
ManageListingButtons::Back => {
|
ManageListingButtons::Back => {
|
||||||
dialogue.update(MyListingsState::ViewingListings).await?;
|
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![];
|
let mut response_lines = vec![];
|
||||||
response_lines.push(format!("<b>{}</b>", &listing.base.title));
|
response_lines.push(format!("<b>{}</b>", &listing.base.title));
|
||||||
if let Some(description) = &listing.base.description {
|
if let Some(description) = &listing.base.description {
|
||||||
response_lines.push(format!("{}", description));
|
response_lines.push(description.to_owned());
|
||||||
}
|
}
|
||||||
send_message(
|
send_message(
|
||||||
bot,
|
bot,
|
||||||
@@ -423,27 +408,13 @@ async fn send_preview_listing_message(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_and_listing(
|
async fn get_listing_for_user(
|
||||||
db_pool: &SqlitePool,
|
db_pool: &SqlitePool,
|
||||||
bot: &Bot,
|
bot: &Bot,
|
||||||
user_id: teloxide::types::UserId,
|
user: PersistedUser,
|
||||||
listing_id: ListingDbId,
|
listing_id: ListingDbId,
|
||||||
target: impl Into<MessageTarget>,
|
target: impl Into<MessageTarget>,
|
||||||
) -> HandlerResult<(PersistedUser, PersistedListing)> {
|
) -> HandlerResult<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"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let listing = match ListingDAO::find_by_id(db_pool, listing_id).await? {
|
let listing = match ListingDAO::find_by_id(db_pool, listing_id).await? {
|
||||||
Some(listing) => listing,
|
Some(listing) => listing,
|
||||||
None => {
|
None => {
|
||||||
@@ -463,5 +434,5 @@ async fn get_user_and_listing(
|
|||||||
return Err(anyhow::anyhow!("User does not own listing"));
|
return Err(anyhow::anyhow!("User does not own listing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((user, listing))
|
Ok(listing)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
commands::{
|
commands::{
|
||||||
new_listing::{
|
new_listing::{
|
||||||
|
enter_select_new_listing_type,
|
||||||
field_processing::transition_to_field,
|
field_processing::transition_to_field,
|
||||||
keyboard::{
|
keyboard::{
|
||||||
DurationKeyboardButtons, ListingTypeKeyboardButtons, SlotsKeyboardButtons,
|
DurationKeyboardButtons, ListingTypeKeyboardButtons, SlotsKeyboardButtons,
|
||||||
@@ -17,7 +18,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
start::enter_main_menu,
|
start::enter_main_menu,
|
||||||
},
|
},
|
||||||
db::{listing::ListingFields, ListingDuration, ListingType, UserDbId},
|
db::{listing::ListingFields, user::PersistedUser, ListingDuration, ListingType},
|
||||||
message_utils::*,
|
message_utils::*,
|
||||||
HandlerResult, RootDialogue,
|
HandlerResult, RootDialogue,
|
||||||
};
|
};
|
||||||
@@ -28,7 +29,7 @@ use teloxide::{types::CallbackQuery, Bot};
|
|||||||
pub async fn handle_selecting_listing_type_callback(
|
pub async fn handle_selecting_listing_type_callback(
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
seller_id: UserDbId,
|
user: PersistedUser,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
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
|
// 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 first field (Title)
|
||||||
transition_to_field(dialogue, ListingField::Title, draft).await?;
|
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:?}");
|
info!("User {from:?} selected callback: {data:?}");
|
||||||
let target = (from, message_id);
|
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" {
|
if data == "cancel" {
|
||||||
return cancel_wizard(&bot, dialogue, target).await;
|
return cancel_wizard(bot, dialogue, target).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unified callback dispatch
|
// Unified callback dispatch
|
||||||
@@ -248,13 +253,12 @@ async fn handle_duration_callback(
|
|||||||
|
|
||||||
/// Cancel the wizard and exit
|
/// Cancel the wizard and exit
|
||||||
pub async fn cancel_wizard(
|
pub async fn cancel_wizard(
|
||||||
bot: &Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
target: impl Into<MessageTarget>,
|
target: impl Into<MessageTarget>,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let target = target.into();
|
let target = target.into();
|
||||||
info!("{target:?} cancelled new listing wizard");
|
info!("{target:?} cancelled new listing wizard");
|
||||||
dialogue.exit().await?;
|
enter_select_new_listing_type(bot, dialogue, target).await?;
|
||||||
send_message(bot, target, "❌ Listing creation cancelled.", None).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use super::{callbacks::*, handlers::*, types::*};
|
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};
|
use teloxide::{dptree, prelude::*, types::Update};
|
||||||
|
|
||||||
// Create the dialogue handler tree for new listing wizard
|
// Create the dialogue handler tree for new listing wizard
|
||||||
@@ -30,8 +33,9 @@ pub fn new_listing_handler() -> Handler {
|
|||||||
Update::filter_callback_query()
|
Update::filter_callback_query()
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
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),
|
.endpoint(handle_selecting_listing_type_callback),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
|
|||||||
@@ -20,41 +20,33 @@ use crate::{
|
|||||||
},
|
},
|
||||||
db::{
|
db::{
|
||||||
listing::{ListingFields, NewListing, PersistedListing},
|
listing::{ListingFields, NewListing, PersistedListing},
|
||||||
ListingDAO, UserDAO,
|
ListingDAO,
|
||||||
},
|
},
|
||||||
message_utils::*,
|
message_utils::*,
|
||||||
DialogueRootState, HandlerResult, RootDialogue,
|
DialogueRootState, HandlerResult, RootDialogue,
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::info;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use teloxide::{prelude::*, types::*, Bot};
|
use teloxide::{prelude::*, types::*, Bot};
|
||||||
|
|
||||||
/// Handle the /newlisting command - starts the dialogue
|
/// Handle the /newlisting command - starts the dialogue
|
||||||
pub(super) async fn handle_new_listing_command(
|
pub(super) async fn handle_new_listing_command(
|
||||||
db_pool: SqlitePool,
|
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
msg: Message,
|
msg: Message,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let user = msg.from.ok_or_else(|| anyhow::anyhow!("User not found"))?;
|
enter_select_new_listing_type(bot, dialogue, msg.chat).await?;
|
||||||
enter_handle_new_listing(db_pool, bot, dialogue, user, msg.chat).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn enter_handle_new_listing(
|
pub async fn enter_select_new_listing_type(
|
||||||
db_pool: SqlitePool,
|
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
user: User,
|
|
||||||
target: impl Into<MessageTarget>,
|
target: impl Into<MessageTarget>,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let user = UserDAO::find_or_create_by_telegram_user(&db_pool, user).await?;
|
|
||||||
|
|
||||||
// Initialize the dialogue to listing type selection state
|
// Initialize the dialogue to listing type selection state
|
||||||
dialogue
|
dialogue
|
||||||
.update(NewListingState::SelectingListingType {
|
.update(NewListingState::SelectingListingType)
|
||||||
seller_id: user.persisted.id,
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
send_message(
|
send_message(
|
||||||
@@ -84,7 +76,7 @@ pub async fn handle_awaiting_draft_field_input(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if is_cancel(text) {
|
if is_cancel(text) {
|
||||||
return cancel_wizard(&bot, dialogue, chat).await;
|
return cancel_wizard(bot, dialogue, chat).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the field update
|
// Process the field update
|
||||||
@@ -145,14 +137,6 @@ pub async fn handle_viewing_draft_callback(
|
|||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
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);
|
let target = (from.clone(), message_id);
|
||||||
|
|
||||||
match ConfirmationKeyboardButtons::try_from(data.as_str())? {
|
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
|
/// Get the appropriate keyboard for a field
|
||||||
pub fn get_keyboard_for_field(field: ListingField) -> Option<InlineKeyboardMarkup> {
|
pub fn get_keyboard_for_field(field: ListingField) -> Option<InlineKeyboardMarkup> {
|
||||||
match field {
|
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::Description => Some(create_skip_cancel_keyboard()),
|
||||||
ListingField::Price => None,
|
ListingField::Price => None,
|
||||||
ListingField::Slots => Some(SlotsKeyboardButtons::to_keyboard()),
|
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 {
|
fn create_skip_cancel_keyboard() -> InlineKeyboardMarkup {
|
||||||
create_multi_row_keyboard(&[&[("Skip", "skip"), ("Cancel", "cancel")]])
|
create_multi_row_keyboard(&[&[("Skip", "skip"), ("Cancel", "cancel")]])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,11 @@ mod keyboard;
|
|||||||
pub mod messages;
|
pub mod messages;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod validations;
|
mod validations;
|
||||||
|
|
||||||
// Re-export the main handler for external use
|
// Re-export the main handler for external use
|
||||||
pub use handler_factory::new_listing_handler;
|
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::*;
|
pub use types::*;
|
||||||
|
|||||||
@@ -89,9 +89,7 @@ pub enum ListingField {
|
|||||||
// Dialogue state for the new listing wizard
|
// Dialogue state for the new listing wizard
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum NewListingState {
|
pub enum NewListingState {
|
||||||
SelectingListingType {
|
SelectingListingType,
|
||||||
seller_id: UserDbId,
|
|
||||||
},
|
|
||||||
AwaitingDraftField {
|
AwaitingDraftField {
|
||||||
field: ListingField,
|
field: ListingField,
|
||||||
draft: ListingDraft,
|
draft: ListingDraft,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use teloxide::{
|
|||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::{
|
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,
|
keyboard_buttons,
|
||||||
message_utils::{extract_callback_data, send_message, MessageTarget},
|
message_utils::{extract_callback_data, send_message, MessageTarget},
|
||||||
Command, DialogueRootState, HandlerResult, RootDialogue,
|
Command, DialogueRootState, HandlerResult, RootDialogue,
|
||||||
@@ -16,9 +17,6 @@ use crate::{
|
|||||||
|
|
||||||
keyboard_buttons! {
|
keyboard_buttons! {
|
||||||
pub enum MainMenuButtons {
|
pub enum MainMenuButtons {
|
||||||
[
|
|
||||||
NewListing("🛍️ New Listing", "menu_new_listing"),
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
MyListings("📋 My Listings", "menu_my_listings"),
|
MyListings("📋 My Listings", "menu_my_listings"),
|
||||||
MyBids("💰 My Bids", "menu_my_bids"),
|
MyBids("💰 My Bids", "menu_my_bids"),
|
||||||
@@ -69,6 +67,7 @@ pub async fn handle_main_menu_callback(
|
|||||||
db_pool: SqlitePool,
|
db_pool: SqlitePool,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
|
user: PersistedUser,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
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())?;
|
let button = MainMenuButtons::try_from(data.as_str())?;
|
||||||
match button {
|
match button {
|
||||||
MainMenuButtons::NewListing => {
|
|
||||||
enter_handle_new_listing(db_pool, bot, dialogue, from.clone(), target).await?;
|
|
||||||
}
|
|
||||||
MainMenuButtons::MyListings => {
|
MainMenuButtons::MyListings => {
|
||||||
// Call show_listings_for_user directly
|
// 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 => {
|
MainMenuButtons::MyBids => {
|
||||||
send_message(
|
send_message(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub type PersistedUser = User<PersistedUserFields>;
|
|||||||
pub type NewUser = User<()>;
|
pub type NewUser = User<()>;
|
||||||
|
|
||||||
/// Core user information
|
/// Core user information
|
||||||
#[derive(Debug, Clone, FromRow)]
|
#[derive(Clone, FromRow)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct User<P: Debug + Clone> {
|
pub struct User<P: Debug + Clone> {
|
||||||
pub persisted: P,
|
pub persisted: P,
|
||||||
@@ -19,6 +19,22 @@ pub struct User<P: Debug + Clone> {
|
|||||||
pub is_banned: bool,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct PersistedUserFields {
|
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),*),*
|
$($($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 {
|
impl From<$name> for teloxide::types::InlineKeyboardButton {
|
||||||
fn from(value: $name) -> Self {
|
fn from(value: $name) -> Self {
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@@ -2,17 +2,21 @@ mod commands;
|
|||||||
mod config;
|
mod config;
|
||||||
mod db;
|
mod db;
|
||||||
mod dptree_utils;
|
mod dptree_utils;
|
||||||
|
mod handler_utils;
|
||||||
mod keyboard_utils;
|
mod keyboard_utils;
|
||||||
mod message_utils;
|
mod message_utils;
|
||||||
mod sqlite_storage;
|
mod sqlite_storage;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_utils;
|
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::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 anyhow::Result;
|
||||||
use commands::*;
|
use commands::*;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
@@ -98,6 +102,7 @@ async fn main() -> Result<()> {
|
|||||||
.branch(
|
.branch(
|
||||||
Update::filter_callback_query().branch(
|
Update::filter_callback_query().branch(
|
||||||
dptree::case![DialogueRootState::MainMenu]
|
dptree::case![DialogueRootState::MainMenu]
|
||||||
|
.filter_map_async(find_or_create_db_user_from_callback_query)
|
||||||
.endpoint(handle_main_menu_callback),
|
.endpoint(handle_main_menu_callback),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user