create App struct
This commit is contained in:
@@ -4,15 +4,17 @@ use crate::{
|
|||||||
case,
|
case,
|
||||||
commands::new_listing::validations::{validate_price, SetFieldError},
|
commands::new_listing::validations::{validate_price, SetFieldError},
|
||||||
db::{
|
db::{
|
||||||
|
bid::NewBid,
|
||||||
listing::{ListingFields, PersistedListing},
|
listing::{ListingFields, PersistedListing},
|
||||||
user::PersistedUser,
|
user::PersistedUser,
|
||||||
ListingDbId, MoneyAmount, UserDAO,
|
BidDAO, ListingDbId, MoneyAmount, UserDAO,
|
||||||
},
|
},
|
||||||
|
dptree_utils::MapTwo,
|
||||||
handle_error::with_error_handler,
|
handle_error::with_error_handler,
|
||||||
handler_utils::find_listing_by_id,
|
handler_utils::find_listing_by_id,
|
||||||
message_utils::{send_message, MessageTarget},
|
message_utils::{MessageTarget, SendHtmlMessage},
|
||||||
start_command_data::StartCommandData,
|
start_command_data::StartCommandData,
|
||||||
BotError, BotHandler, BotResult, DialogueRootState, RootDialogue,
|
App, BotError, BotHandler, BotResult, DialogueRootState, RootDialogue,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use log::info;
|
use log::info;
|
||||||
@@ -20,7 +22,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
use teloxide::{
|
use teloxide::{
|
||||||
dispatching::UpdateFilterExt,
|
dispatching::UpdateFilterExt,
|
||||||
types::{CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update},
|
types::{CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update},
|
||||||
Bot,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -40,8 +41,11 @@ pub fn bidding_handler() -> BotHandler {
|
|||||||
Update::filter_message()
|
Update::filter_message()
|
||||||
.filter_map(StartCommandData::get_from_update)
|
.filter_map(StartCommandData::get_from_update)
|
||||||
.filter_map(StartCommandData::get_place_bid_on_listing_start_command)
|
.filter_map(StartCommandData::get_place_bid_on_listing_start_command)
|
||||||
.filter_map_async(find_listing_by_id)
|
.branch(
|
||||||
.endpoint(with_error_handler(handle_place_bid_on_listing)),
|
dptree::entry()
|
||||||
|
.filter_map_async(find_listing_by_id)
|
||||||
|
.endpoint(with_error_handler(handle_place_bid_on_listing)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
Update::filter_message()
|
Update::filter_message()
|
||||||
@@ -56,11 +60,12 @@ pub fn bidding_handler() -> BotHandler {
|
|||||||
.chain(case![DialogueRootState::Bidding(
|
.chain(case![DialogueRootState::Bidding(
|
||||||
BiddingState::AwaitingConfirmBidAmount(listing_id, bid_amount)
|
BiddingState::AwaitingConfirmBidAmount(listing_id, bid_amount)
|
||||||
)])
|
)])
|
||||||
.filter_map_async(
|
.map2(|(listing_id, bid_amount): (ListingDbId, MoneyAmount)| {
|
||||||
async |listing_dao, (listing_id, _): (ListingDbId, MoneyAmount)| {
|
(listing_id, bid_amount)
|
||||||
find_listing_by_id(listing_dao, listing_id).await
|
})
|
||||||
},
|
.filter_map_async(async |listing_dao, listing_id| {
|
||||||
)
|
find_listing_by_id(listing_dao, listing_id).await
|
||||||
|
})
|
||||||
.endpoint(with_error_handler(
|
.endpoint(with_error_handler(
|
||||||
handle_awaiting_confirm_bid_amount_callback,
|
handle_awaiting_confirm_bid_amount_callback,
|
||||||
)),
|
)),
|
||||||
@@ -68,7 +73,7 @@ pub fn bidding_handler() -> BotHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_place_bid_on_listing(
|
async fn handle_place_bid_on_listing(
|
||||||
bot: Bot,
|
app: App,
|
||||||
user_dao: UserDAO,
|
user_dao: UserDAO,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
@@ -110,13 +115,15 @@ async fn handle_place_bid_on_listing(
|
|||||||
let keyboard = InlineKeyboardMarkup::default()
|
let keyboard = InlineKeyboardMarkup::default()
|
||||||
.append_row([InlineKeyboardButton::callback("Bid $1", "cancel")]);
|
.append_row([InlineKeyboardButton::callback("Bid $1", "cancel")]);
|
||||||
|
|
||||||
send_message(&bot, target, response_lines.join("\n"), Some(keyboard)).await?;
|
app.bot
|
||||||
|
.send_html_message(target, response_lines.join("\n"), Some(keyboard))
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_awaiting_bid_amount_input(
|
async fn handle_awaiting_bid_amount_input(
|
||||||
bot: Bot,
|
app: App,
|
||||||
listing: PersistedListing,
|
listing: PersistedListing,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
@@ -136,19 +143,20 @@ async fn handle_awaiting_bid_amount_input(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
send_message(
|
let bid_amount_str = format!("{}{}", listing.base.currency_type.symbol(), bid_amount);
|
||||||
&bot,
|
app.bot
|
||||||
target,
|
.send_html_message(
|
||||||
format!("Confirm bid amount: {bid_amount} - this cannot be undone!"),
|
target,
|
||||||
Some(InlineKeyboardMarkup::default().append_row([
|
format!("Confirm bid amount: {bid_amount_str} - this cannot be undone!"),
|
||||||
InlineKeyboardButton::callback(
|
Some(InlineKeyboardMarkup::default().append_row([
|
||||||
format!("Confirm bid amount: {bid_amount}"),
|
InlineKeyboardButton::callback(
|
||||||
"confirm_bid",
|
format!("Confirm bid amount: {bid_amount_str}"),
|
||||||
),
|
"confirm_bid",
|
||||||
InlineKeyboardButton::callback("Cancel", "cancel_bid"),
|
),
|
||||||
])),
|
InlineKeyboardButton::callback("Cancel", "cancel_bid"),
|
||||||
)
|
])),
|
||||||
.await?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
dialogue
|
dialogue
|
||||||
.update(BiddingState::AwaitingConfirmBidAmount(
|
.update(BiddingState::AwaitingConfirmBidAmount(
|
||||||
@@ -162,9 +170,11 @@ async fn handle_awaiting_bid_amount_input(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_awaiting_confirm_bid_amount_callback(
|
async fn handle_awaiting_confirm_bid_amount_callback(
|
||||||
bot: Bot,
|
app: App,
|
||||||
listing: PersistedListing,
|
listing: PersistedListing,
|
||||||
(_, bid_amount): (ListingDbId, MoneyAmount),
|
user: PersistedUser,
|
||||||
|
bid_dao: BidDAO,
|
||||||
|
bid_amount: MoneyAmount,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
@@ -178,7 +188,9 @@ async fn handle_awaiting_confirm_bid_amount_callback(
|
|||||||
"confirm_bid" => bid_amount,
|
"confirm_bid" => bid_amount,
|
||||||
"cancel_bid" => {
|
"cancel_bid" => {
|
||||||
dialogue.exit().await.context("failed to exit dialogue")?;
|
dialogue.exit().await.context("failed to exit dialogue")?;
|
||||||
send_message(&bot, target, "Bid cancelled", None).await?;
|
app.bot
|
||||||
|
.send_html_message(target, "Bid cancelled", None)
|
||||||
|
.await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -188,20 +200,19 @@ async fn handle_awaiting_confirm_bid_amount_callback(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let bid = NewBid::new_basic(listing.persisted.id, user.persisted.id, bid_amount);
|
||||||
|
bid_dao.insert_bid(bid).await?;
|
||||||
|
|
||||||
dialogue.exit().await.context("failed to exit dialogue")?;
|
dialogue.exit().await.context("failed to exit dialogue")?;
|
||||||
|
|
||||||
send_message(
|
let bid_amount_str = format!("{}{}", listing.base.currency_type.symbol(), bid_amount);
|
||||||
&bot,
|
app.bot
|
||||||
target.only_chat_id(),
|
.send_html_message(
|
||||||
format!(
|
target.only_chat_id(),
|
||||||
"Bid placed for {}{} on {}",
|
format!("Bid placed for {bid_amount_str} on {}", listing.base.title),
|
||||||
listing.base.currency_type.symbol(),
|
None,
|
||||||
bid_amount,
|
)
|
||||||
listing.base.title
|
.await?;
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// TODO - keyboard with buttons to:
|
// TODO - keyboard with buttons to:
|
||||||
// - be notified if they are outbid
|
// - be notified if they are outbid
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
use crate::{
|
use crate::{message_utils::{MessageTarget, SendHtmlMessage}, App, BotResult, Command};
|
||||||
message_utils::{send_message, MessageTarget},
|
use teloxide::utils::command::BotCommands;
|
||||||
BotResult, Command,
|
|
||||||
};
|
|
||||||
use teloxide::{utils::command::BotCommands, Bot};
|
|
||||||
|
|
||||||
pub async fn handle_help(bot: Bot, target: MessageTarget) -> BotResult {
|
pub async fn handle_help(app: App, target: MessageTarget) -> BotResult {
|
||||||
let help_message = format!(
|
let help_message = format!(
|
||||||
"📋 Available Commands:\n\n{}\n\n\
|
"📋 Available Commands:\n\n{}\n\n\
|
||||||
📧 Support: Contact @admin for help\n\
|
📧 Support: Contact @admin for help\n\
|
||||||
@@ -12,6 +9,8 @@ pub async fn handle_help(bot: Bot, target: MessageTarget) -> BotResult {
|
|||||||
Command::descriptions()
|
Command::descriptions()
|
||||||
);
|
);
|
||||||
|
|
||||||
send_message(&bot, target, help_message, None).await?;
|
app.bot
|
||||||
|
.send_html_message(target, help_message, None)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
use crate::{
|
use crate::{message_utils::{MessageTarget, SendHtmlMessage}, App, BotResult};
|
||||||
message_utils::{send_message, MessageTarget},
|
|
||||||
BotResult,
|
|
||||||
};
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use teloxide::{types::Message, Bot};
|
use teloxide::types::Message;
|
||||||
|
|
||||||
pub async fn handle_my_bids(bot: Bot, msg: Message, target: MessageTarget) -> BotResult {
|
pub async fn handle_my_bids(app: App, msg: Message, target: MessageTarget) -> BotResult {
|
||||||
let response = "🎯 My Bids (Coming Soon)\n\n\
|
let response = "🎯 My Bids (Coming Soon)\n\n\
|
||||||
Here you'll be able to view:\n\
|
Here you'll be able to view:\n\
|
||||||
• Your active bids\n\
|
• Your active bids\n\
|
||||||
@@ -20,6 +17,6 @@ pub async fn handle_my_bids(bot: Bot, msg: Message, target: MessageTarget) -> Bo
|
|||||||
msg.chat.id
|
msg.chat.id
|
||||||
);
|
);
|
||||||
|
|
||||||
send_message(&bot, target, response, None).await?;
|
app.bot.send_html_message(target, response, None).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ use crate::{
|
|||||||
db::{
|
db::{
|
||||||
listing::{ListingFields, PersistedListing},
|
listing::{ListingFields, PersistedListing},
|
||||||
user::PersistedUser,
|
user::PersistedUser,
|
||||||
ListingDAO, ListingDbId, ListingType,
|
DAOs, ListingDbId, ListingType,
|
||||||
},
|
},
|
||||||
handle_error::with_error_handler,
|
handle_error::with_error_handler,
|
||||||
handler_utils::{find_listing_by_id, find_or_create_db_user_from_update},
|
handler_utils::{find_listing_by_id, find_or_create_db_user_from_update},
|
||||||
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
|
message_utils::{extract_callback_data, pluralize_with_count, MessageTarget, SendHtmlMessage},
|
||||||
start_command_data::StartCommandData,
|
start_command_data::StartCommandData,
|
||||||
BotError, BotResult, Command, DialogueRootState, RootDialogue,
|
App, BotError, BotResult, Command, DialogueRootState, RootDialogue,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
||||||
@@ -87,34 +87,40 @@ pub fn my_listings_handler() -> Handler<'static, BotResult, DpHandlerDescription
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_view_listing_details(
|
async fn handle_view_listing_details(
|
||||||
bot: Bot,
|
app: App,
|
||||||
listing: PersistedListing,
|
listing: PersistedListing,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
send_listing_details_message(&bot, target, listing, None).await?;
|
send_listing_details_message(app, target, listing, None).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn inline_query_extract_forward_listing(
|
async fn inline_query_extract_forward_listing(
|
||||||
listing_dao: ListingDAO,
|
app: App,
|
||||||
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 = listing_dao.find_by_id(listing_id).await.unwrap_or(None)?;
|
let listing = app
|
||||||
|
.daos
|
||||||
|
.listing
|
||||||
|
.find_by_id(listing_id)
|
||||||
|
.await
|
||||||
|
.unwrap_or(None)?;
|
||||||
Some(listing)
|
Some(listing)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_forward_listing(
|
async fn handle_forward_listing(
|
||||||
bot: Bot,
|
app: App,
|
||||||
inline_query: InlineQuery,
|
inline_query: InlineQuery,
|
||||||
listing: PersistedListing,
|
listing: PersistedListing,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
info!("Handling forward listing inline query for listing {listing:?}");
|
info!("Handling forward listing inline query for listing {listing:?}");
|
||||||
|
|
||||||
let bot_username = match bot
|
let bot_username = match app
|
||||||
|
.bot
|
||||||
.get_me()
|
.get_me()
|
||||||
.await
|
.await
|
||||||
.context("failed to get bot username")?
|
.context("failed to get bot username")?
|
||||||
@@ -175,22 +181,23 @@ async fn handle_forward_listing(
|
|||||||
listing.base.ends_at.format("%b %d, %Y at %H:%M UTC")
|
listing.base.ends_at.format("%b %d, %Y at %H:%M UTC")
|
||||||
);
|
);
|
||||||
|
|
||||||
bot.answer_inline_query(
|
app.bot
|
||||||
inline_query.id,
|
.answer_inline_query(
|
||||||
[InlineQueryResult::Article(
|
inline_query.id,
|
||||||
InlineQueryResultArticle::new(
|
[InlineQueryResult::Article(
|
||||||
listing.persisted.id.to_string(),
|
InlineQueryResultArticle::new(
|
||||||
format!("💰 {} - ${}", listing.base.title, current_price),
|
listing.persisted.id.to_string(),
|
||||||
InputMessageContent::Text(
|
format!("💰 {} - ${}", listing.base.title, current_price),
|
||||||
InputMessageContentText::new(message_content).parse_mode(ParseMode::Html),
|
InputMessageContent::Text(
|
||||||
),
|
InputMessageContentText::new(message_content).parse_mode(ParseMode::Html),
|
||||||
)
|
),
|
||||||
.description(&listing.base.title)
|
)
|
||||||
.reply_markup(keyboard), // Add the inline keyboard here!
|
.description(&listing.base.title)
|
||||||
)],
|
.reply_markup(keyboard), // Add the inline keyboard here!
|
||||||
)
|
)],
|
||||||
.await
|
)
|
||||||
.map_err(|e| anyhow::anyhow!("Error answering inline query: {e:?}"))?;
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Error answering inline query: {e:?}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,19 +226,17 @@ fn get_listing_current_price(listing: &PersistedListing) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_my_listings_command_input(
|
async fn handle_my_listings_command_input(
|
||||||
listing_dao: ListingDAO,
|
app: App,
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
enter_my_listings(listing_dao, bot, dialogue, user, target, None).await?;
|
enter_my_listings(app, dialogue, user, target, None).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn enter_my_listings(
|
pub async fn enter_my_listings(
|
||||||
listing_dao: ListingDAO,
|
app: App,
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
@@ -243,7 +248,7 @@ pub async fn enter_my_listings(
|
|||||||
.await
|
.await
|
||||||
.context("failed to update dialogue")?;
|
.context("failed to update dialogue")?;
|
||||||
|
|
||||||
let listings = listing_dao.find_by_seller(user.persisted.id).await?;
|
let listings = app.daos.listing.find_by_seller(user.persisted.id).await?;
|
||||||
// Create keyboard with buttons for each listing
|
// Create keyboard with buttons for each listing
|
||||||
let mut keyboard = teloxide::types::InlineKeyboardMarkup::default();
|
let mut keyboard = teloxide::types::InlineKeyboardMarkup::default();
|
||||||
for listing in &listings {
|
for listing in &listings {
|
||||||
@@ -255,14 +260,14 @@ pub async fn enter_my_listings(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if listings.is_empty() {
|
if listings.is_empty() {
|
||||||
send_message(
|
app.bot
|
||||||
&bot,
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
"📋 <b>My Listings</b>\n\n\
|
"📋 <b>My Listings</b>\n\n\
|
||||||
You don't have any listings yet.",
|
You don't have any listings yet.",
|
||||||
Some(keyboard),
|
Some(keyboard),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,34 +282,34 @@ pub async fn enter_my_listings(
|
|||||||
response = format!("{flash}\n\n{response}");
|
response = format!("{flash}\n\n{response}");
|
||||||
}
|
}
|
||||||
|
|
||||||
send_message(&bot, target, response, Some(keyboard)).await?;
|
app.bot
|
||||||
|
.send_html_message(target, response, Some(keyboard))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_viewing_listings_callback(
|
async fn handle_viewing_listings_callback(
|
||||||
listing_dao: ListingDAO,
|
app: App,
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
let data = extract_callback_data(&bot, callback_query).await?;
|
let data = extract_callback_data(&app.bot, callback_query).await?;
|
||||||
|
|
||||||
if let Ok(NavKeyboardButtons::Back) = NavKeyboardButtons::try_from(data.as_str()) {
|
if let Ok(NavKeyboardButtons::Back) = NavKeyboardButtons::try_from(data.as_str()) {
|
||||||
return enter_main_menu(bot, dialogue, target).await;
|
return enter_main_menu(app, dialogue, target).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's the back to menu button
|
// Check if it's the back to menu button
|
||||||
let button = MyListingsButtons::try_from(data.as_str())?;
|
let button = MyListingsButtons::try_from(data.as_str())?;
|
||||||
match button {
|
match button {
|
||||||
MyListingsButtons::SelectListing(listing_id) => {
|
MyListingsButtons::SelectListing(listing_id) => {
|
||||||
let listing = get_listing_for_user(&listing_dao, user, listing_id).await?;
|
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(&bot, dialogue, listing, target).await?;
|
|
||||||
}
|
}
|
||||||
MyListingsButtons::NewListing => {
|
MyListingsButtons::NewListing => {
|
||||||
enter_select_new_listing_type(bot, dialogue, target).await?;
|
enter_select_new_listing_type(app, dialogue, target).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +317,7 @@ async fn handle_viewing_listings_callback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn enter_show_listing_details(
|
async fn enter_show_listing_details(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
listing: PersistedListing,
|
listing: PersistedListing,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
@@ -335,12 +340,12 @@ async fn enter_show_listing_details(
|
|||||||
ManageListingButtons::Delete.to_button(),
|
ManageListingButtons::Delete.to_button(),
|
||||||
])
|
])
|
||||||
.append_row([ManageListingButtons::Back.to_button()]);
|
.append_row([ManageListingButtons::Back.to_button()]);
|
||||||
send_listing_details_message(bot, target, listing, Some(keyboard)).await?;
|
send_listing_details_message(app, target, listing, Some(keyboard)).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_listing_details_message(
|
async fn send_listing_details_message(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
listing: PersistedListing,
|
listing: PersistedListing,
|
||||||
keyboard: Option<InlineKeyboardMarkup>,
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
@@ -357,13 +362,14 @@ async fn send_listing_details_message(
|
|||||||
};
|
};
|
||||||
response_lines.push(format!("<b>{}:</b> {}", step.field_name, field_value));
|
response_lines.push(format!("<b>{}:</b> {}", step.field_name, field_value));
|
||||||
}
|
}
|
||||||
send_message(bot, target, response_lines.join("\n"), keyboard).await?;
|
app.bot
|
||||||
|
.send_html_message(target, response_lines.join("\n"), keyboard)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_managing_listing_callback(
|
async fn handle_managing_listing_callback(
|
||||||
listing_dao: ListingDAO,
|
app: App,
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
@@ -371,29 +377,30 @@ async fn handle_managing_listing_callback(
|
|||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
let from = callback_query.from.clone();
|
let from = callback_query.from.clone();
|
||||||
let data = extract_callback_data(&bot, callback_query).await?;
|
let data = extract_callback_data(&app.bot, callback_query).await?;
|
||||||
|
|
||||||
match ManageListingButtons::try_from(data.as_str())? {
|
match ManageListingButtons::try_from(data.as_str())? {
|
||||||
ManageListingButtons::PreviewMessage => {
|
ManageListingButtons::PreviewMessage => {
|
||||||
let listing = listing_dao
|
let listing = app
|
||||||
|
.daos
|
||||||
|
.listing
|
||||||
.find_by_id(listing_id)
|
.find_by_id(listing_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(anyhow::anyhow!("Listing not found"))?;
|
.ok_or(anyhow::anyhow!("Listing not found"))?;
|
||||||
send_preview_listing_message(&bot, listing, from).await?;
|
send_preview_listing_message(app, listing, from).await?;
|
||||||
}
|
}
|
||||||
ManageListingButtons::ForwardListing => {
|
ManageListingButtons::ForwardListing => {
|
||||||
unimplemented!("Forward listing not implemented");
|
unimplemented!("Forward listing not implemented");
|
||||||
}
|
}
|
||||||
ManageListingButtons::Edit => {
|
ManageListingButtons::Edit => {
|
||||||
let listing = get_listing_for_user(&listing_dao, user, listing_id).await?;
|
let listing = get_listing_for_user(&app.daos, user, listing_id).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(app, target, draft, dialogue, None).await?;
|
||||||
}
|
}
|
||||||
ManageListingButtons::Delete => {
|
ManageListingButtons::Delete => {
|
||||||
listing_dao.delete_listing(listing_id).await?;
|
app.daos.listing.delete_listing(listing_id).await?;
|
||||||
enter_my_listings(
|
enter_my_listings(
|
||||||
listing_dao,
|
app,
|
||||||
bot,
|
|
||||||
dialogue,
|
dialogue,
|
||||||
user,
|
user,
|
||||||
target,
|
target,
|
||||||
@@ -402,7 +409,7 @@ async fn handle_managing_listing_callback(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
ManageListingButtons::Back => {
|
ManageListingButtons::Back => {
|
||||||
enter_my_listings(listing_dao, bot, dialogue, user, target, None).await?;
|
enter_my_listings(app, dialogue, user, target, None).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +448,7 @@ fn keyboard_for_listing(listing: &PersistedListing) -> InlineKeyboardMarkup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn send_preview_listing_message(
|
async fn send_preview_listing_message(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
listing: PersistedListing,
|
listing: PersistedListing,
|
||||||
from: User,
|
from: User,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
@@ -450,22 +457,22 @@ async fn send_preview_listing_message(
|
|||||||
if let Some(description) = &listing.base.description {
|
if let Some(description) = &listing.base.description {
|
||||||
response_lines.push(description.to_owned());
|
response_lines.push(description.to_owned());
|
||||||
}
|
}
|
||||||
send_message(
|
app.bot
|
||||||
bot,
|
.send_html_message(
|
||||||
from.into(),
|
from.into(),
|
||||||
response_lines.join("\n\n"),
|
response_lines.join("\n\n"),
|
||||||
Some(keyboard_for_listing(&listing)),
|
Some(keyboard_for_listing(&listing)),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_listing_for_user(
|
async fn get_listing_for_user(
|
||||||
listing_dao: &ListingDAO,
|
daos: &DAOs,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
listing_id: ListingDbId,
|
listing_id: ListingDbId,
|
||||||
) -> BotResult<PersistedListing> {
|
) -> BotResult<PersistedListing> {
|
||||||
let listing = match listing_dao.find_by_id(listing_id).await? {
|
let listing = match daos.listing.find_by_id(listing_id).await? {
|
||||||
Some(listing) => listing,
|
Some(listing) => listing,
|
||||||
None => {
|
None => {
|
||||||
return Err(BotError::UserVisibleError("❌ Listing not found.".into()));
|
return Err(BotError::UserVisibleError("❌ Listing not found.".into()));
|
||||||
|
|||||||
@@ -15,29 +15,26 @@ use crate::{
|
|||||||
ui::enter_confirm_save_listing,
|
ui::enter_confirm_save_listing,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
db::{
|
db::{user::PersistedUser, CurrencyType, ListingDuration, ListingType, MoneyAmount},
|
||||||
user::PersistedUser, CurrencyType, ListingDAO, ListingDuration, ListingType, MoneyAmount,
|
|
||||||
},
|
|
||||||
message_utils::*,
|
message_utils::*,
|
||||||
BotResult, RootDialogue,
|
App, BotResult, RootDialogue,
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use teloxide::{types::CallbackQuery, Bot};
|
use teloxide::types::CallbackQuery;
|
||||||
|
|
||||||
/// Handle callbacks during the listing type selection phase
|
/// Handle callbacks during the listing type selection phase
|
||||||
pub async fn handle_selecting_listing_type_callback(
|
pub async fn handle_selecting_listing_type_callback(
|
||||||
listing_dao: ListingDAO,
|
app: App,
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
let data = extract_callback_data(&bot, callback_query).await?;
|
let data = extract_callback_data(&app.bot, callback_query).await?;
|
||||||
info!("User {target:?} selected listing type: {data:?}");
|
info!("User {target:?} selected listing type: {data:?}");
|
||||||
|
|
||||||
if let Ok(NavKeyboardButtons::Back) = NavKeyboardButtons::try_from(data.as_str()) {
|
if let Ok(NavKeyboardButtons::Back) = NavKeyboardButtons::try_from(data.as_str()) {
|
||||||
return enter_my_listings(listing_dao, bot, dialogue, user, target, None).await;
|
return enter_my_listings(app, dialogue, user, target, None).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the listing type from callback data
|
// Parse the listing type from callback data
|
||||||
@@ -64,38 +61,38 @@ pub async fn handle_selecting_listing_type_callback(
|
|||||||
get_step_message(ListingField::Title, listing_type)
|
get_step_message(ListingField::Title, listing_type)
|
||||||
);
|
);
|
||||||
|
|
||||||
send_message(
|
app.bot
|
||||||
&bot,
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
response,
|
response,
|
||||||
get_keyboard_for_field(ListingField::Title),
|
get_keyboard_for_field(ListingField::Title),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle callbacks during the field input phase
|
/// Handle callbacks during the field input phase
|
||||||
pub async fn handle_awaiting_draft_field_callback(
|
pub async fn handle_awaiting_draft_field_callback(
|
||||||
bot: Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
(field, draft): (ListingField, ListingDraft),
|
(field, draft): (ListingField, ListingDraft),
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
let data = extract_callback_data(&bot, callback_query).await?;
|
let data = extract_callback_data(&app.bot, callback_query).await?;
|
||||||
info!("User {target:?} selected callback: {data:?}");
|
info!("User {target:?} selected callback: {data:?}");
|
||||||
|
|
||||||
if let Ok(button) = NavKeyboardButtons::try_from(data.as_str()) {
|
if let Ok(button) = NavKeyboardButtons::try_from(data.as_str()) {
|
||||||
match button {
|
match button {
|
||||||
NavKeyboardButtons::Back => {
|
NavKeyboardButtons::Back => {
|
||||||
return enter_select_new_listing_type(bot, dialogue, target).await;
|
return enter_select_new_listing_type(app, dialogue, target).await;
|
||||||
}
|
}
|
||||||
NavKeyboardButtons::Skip => {
|
NavKeyboardButtons::Skip => {
|
||||||
return handle_skip_field(&bot, dialogue, field, draft, target).await;
|
return handle_skip_field(app, dialogue, field, draft, target).await;
|
||||||
}
|
}
|
||||||
NavKeyboardButtons::Cancel => {
|
NavKeyboardButtons::Cancel => {
|
||||||
return cancel_wizard(bot, dialogue, target).await;
|
return cancel_wizard(app, dialogue, target).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,23 +101,23 @@ pub async fn handle_awaiting_draft_field_callback(
|
|||||||
match field {
|
match field {
|
||||||
ListingField::Slots => {
|
ListingField::Slots => {
|
||||||
let button = SlotsKeyboardButtons::try_from(data.as_str())?;
|
let button = SlotsKeyboardButtons::try_from(data.as_str())?;
|
||||||
handle_slots_callback(&bot, dialogue, draft, button, target).await
|
handle_slots_callback(app, dialogue, draft, button, target).await
|
||||||
}
|
}
|
||||||
ListingField::StartTime => {
|
ListingField::StartTime => {
|
||||||
let button = StartTimeKeyboardButtons::try_from(data.as_str())?;
|
let button = StartTimeKeyboardButtons::try_from(data.as_str())?;
|
||||||
handle_start_time_callback(&bot, dialogue, draft, button, target).await
|
handle_start_time_callback(app, dialogue, draft, button, target).await
|
||||||
}
|
}
|
||||||
ListingField::EndTime => {
|
ListingField::EndTime => {
|
||||||
let button = DurationKeyboardButtons::try_from(data.as_str())?;
|
let button = DurationKeyboardButtons::try_from(data.as_str())?;
|
||||||
handle_duration_callback(&bot, dialogue, draft, button, target).await
|
handle_duration_callback(app, dialogue, draft, button, target).await
|
||||||
}
|
}
|
||||||
ListingField::MinBidIncrement => {
|
ListingField::MinBidIncrement => {
|
||||||
let button = EditMinimumBidIncrementKeyboardButtons::try_from(data.as_str())?;
|
let button = EditMinimumBidIncrementKeyboardButtons::try_from(data.as_str())?;
|
||||||
handle_starting_bid_amount_callback(&bot, dialogue, draft, button, target).await
|
handle_starting_bid_amount_callback(app, dialogue, draft, button, target).await
|
||||||
}
|
}
|
||||||
ListingField::CurrencyType => {
|
ListingField::CurrencyType => {
|
||||||
let button = CurrencyTypeKeyboardButtons::try_from(data.as_str())?;
|
let button = CurrencyTypeKeyboardButtons::try_from(data.as_str())?;
|
||||||
handle_currency_type_callback(&bot, dialogue, draft, button, target).await
|
handle_currency_type_callback(app, dialogue, draft, button, target).await
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
error!("Unknown callback data for field {field:?}: {data}");
|
error!("Unknown callback data for field {field:?}: {data}");
|
||||||
@@ -130,7 +127,7 @@ pub async fn handle_awaiting_draft_field_callback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_skip_field(
|
async fn handle_skip_field(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
current_field: ListingField,
|
current_field: ListingField,
|
||||||
draft: ListingDraft,
|
draft: ListingDraft,
|
||||||
@@ -146,16 +143,18 @@ async fn handle_skip_field(
|
|||||||
get_step_message(next_field, draft.listing_type())
|
get_step_message(next_field, draft.listing_type())
|
||||||
);
|
);
|
||||||
transition_to_field(dialogue, next_field, draft).await?;
|
transition_to_field(dialogue, next_field, draft).await?;
|
||||||
send_message(bot, target, response, get_keyboard_for_field(next_field)).await?;
|
app.bot
|
||||||
|
.send_html_message(target, response, get_keyboard_for_field(next_field))
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
enter_confirm_save_listing(bot, dialogue, target, draft, Some(flash)).await?;
|
enter_confirm_save_listing(app, dialogue, target, draft, Some(flash)).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle slots selection callback
|
/// Handle slots selection callback
|
||||||
async fn handle_slots_callback(
|
async fn handle_slots_callback(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
mut draft: ListingDraft,
|
mut draft: ListingDraft,
|
||||||
button: SlotsKeyboardButtons,
|
button: SlotsKeyboardButtons,
|
||||||
@@ -180,19 +179,19 @@ async fn handle_slots_callback(
|
|||||||
get_step_message(ListingField::StartTime, draft.listing_type())
|
get_step_message(ListingField::StartTime, draft.listing_type())
|
||||||
);
|
);
|
||||||
transition_to_field(dialogue, ListingField::StartTime, draft).await?;
|
transition_to_field(dialogue, ListingField::StartTime, draft).await?;
|
||||||
send_message(
|
app.bot
|
||||||
bot,
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
&response,
|
&response,
|
||||||
get_keyboard_for_field(ListingField::StartTime),
|
get_keyboard_for_field(ListingField::StartTime),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle start time selection callback
|
/// Handle start time selection callback
|
||||||
async fn handle_start_time_callback(
|
async fn handle_start_time_callback(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
mut draft: ListingDraft,
|
mut draft: ListingDraft,
|
||||||
button: StartTimeKeyboardButtons,
|
button: StartTimeKeyboardButtons,
|
||||||
@@ -215,19 +214,19 @@ async fn handle_start_time_callback(
|
|||||||
get_step_message(ListingField::EndTime, draft.listing_type())
|
get_step_message(ListingField::EndTime, draft.listing_type())
|
||||||
);
|
);
|
||||||
transition_to_field(dialogue, ListingField::EndTime, draft).await?;
|
transition_to_field(dialogue, ListingField::EndTime, draft).await?;
|
||||||
send_message(
|
app.bot
|
||||||
bot,
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
&response,
|
&response,
|
||||||
get_keyboard_for_field(ListingField::EndTime),
|
get_keyboard_for_field(ListingField::EndTime),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle duration selection callback
|
/// Handle duration selection callback
|
||||||
async fn handle_duration_callback(
|
async fn handle_duration_callback(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
mut draft: ListingDraft,
|
mut draft: ListingDraft,
|
||||||
button: DurationKeyboardButtons,
|
button: DurationKeyboardButtons,
|
||||||
@@ -248,11 +247,11 @@ async fn handle_duration_callback(
|
|||||||
.map_err(|e| anyhow::anyhow!("Error updating duration: {e:?}"))?;
|
.map_err(|e| anyhow::anyhow!("Error updating duration: {e:?}"))?;
|
||||||
|
|
||||||
let flash = get_success_message(ListingField::EndTime, draft.listing_type());
|
let flash = get_success_message(ListingField::EndTime, draft.listing_type());
|
||||||
enter_confirm_save_listing(bot, dialogue, target, draft, Some(flash)).await
|
enter_confirm_save_listing(app, dialogue, target, draft, Some(flash)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_starting_bid_amount_callback(
|
async fn handle_starting_bid_amount_callback(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
mut draft: ListingDraft,
|
mut draft: ListingDraft,
|
||||||
button: EditMinimumBidIncrementKeyboardButtons,
|
button: EditMinimumBidIncrementKeyboardButtons,
|
||||||
@@ -273,11 +272,11 @@ async fn handle_starting_bid_amount_callback(
|
|||||||
.map_err(|e| anyhow::anyhow!("Error updating starting bid amount: {e:?}"))?;
|
.map_err(|e| anyhow::anyhow!("Error updating starting bid amount: {e:?}"))?;
|
||||||
|
|
||||||
let flash = get_success_message(ListingField::StartingBidAmount, draft.listing_type());
|
let flash = get_success_message(ListingField::StartingBidAmount, draft.listing_type());
|
||||||
enter_confirm_save_listing(bot, dialogue, target, draft, Some(flash)).await
|
enter_confirm_save_listing(app, dialogue, target, draft, Some(flash)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_currency_type_callback(
|
async fn handle_currency_type_callback(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
mut draft: ListingDraft,
|
mut draft: ListingDraft,
|
||||||
button: CurrencyTypeKeyboardButtons,
|
button: CurrencyTypeKeyboardButtons,
|
||||||
@@ -304,13 +303,15 @@ async fn handle_currency_type_callback(
|
|||||||
get_step_message(next_field, draft.listing_type())
|
get_step_message(next_field, draft.listing_type())
|
||||||
);
|
);
|
||||||
transition_to_field(dialogue, next_field, draft).await?;
|
transition_to_field(dialogue, next_field, draft).await?;
|
||||||
send_message(bot, target, &response, get_keyboard_for_field(next_field)).await?;
|
app.bot
|
||||||
|
.send_html_message(target, &response, get_keyboard_for_field(next_field))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancel the wizard and exit
|
/// Cancel the wizard and exit
|
||||||
pub async fn cancel_wizard(bot: Bot, dialogue: RootDialogue, target: MessageTarget) -> BotResult {
|
pub async fn cancel_wizard(app: App, dialogue: RootDialogue, target: MessageTarget) -> BotResult {
|
||||||
info!("{target:?} cancelled new listing wizard");
|
info!("{target:?} cancelled new listing wizard");
|
||||||
enter_select_new_listing_type(bot, dialogue, target).await?;
|
enter_select_new_listing_type(app, dialogue, target).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,24 +28,24 @@ use crate::{
|
|||||||
ListingDAO,
|
ListingDAO,
|
||||||
},
|
},
|
||||||
message_utils::*,
|
message_utils::*,
|
||||||
BotError, BotResult, DialogueRootState, RootDialogue,
|
App, BotError, BotResult, DialogueRootState, RootDialogue,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use log::info;
|
use log::info;
|
||||||
use teloxide::{prelude::*, types::*, Bot};
|
use teloxide::{prelude::*, types::*};
|
||||||
|
|
||||||
/// 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(
|
||||||
bot: Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
enter_select_new_listing_type(bot, dialogue, target).await?;
|
enter_select_new_listing_type(app, dialogue, target).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn enter_select_new_listing_type(
|
pub async fn enter_select_new_listing_type(
|
||||||
bot: Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
@@ -55,19 +55,19 @@ pub async fn enter_select_new_listing_type(
|
|||||||
.await
|
.await
|
||||||
.context("failed to update dialogue")?;
|
.context("failed to update dialogue")?;
|
||||||
|
|
||||||
send_message(
|
app.bot
|
||||||
&bot,
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
get_listing_type_selection_message(),
|
get_listing_type_selection_message(),
|
||||||
Some(get_listing_type_keyboard()),
|
Some(get_listing_type_keyboard()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle text input for any field during creation
|
/// Handle text input for any field during creation
|
||||||
pub async fn handle_awaiting_draft_field_input(
|
pub async fn handle_awaiting_draft_field_input(
|
||||||
bot: Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
(field, mut draft): (ListingField, ListingDraft),
|
(field, mut draft): (ListingField, ListingDraft),
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
@@ -97,17 +97,19 @@ pub async fn handle_awaiting_draft_field_input(
|
|||||||
get_step_message(next_field, draft.listing_type())
|
get_step_message(next_field, draft.listing_type())
|
||||||
);
|
);
|
||||||
transition_to_field(dialogue, next_field, draft).await?;
|
transition_to_field(dialogue, next_field, draft).await?;
|
||||||
send_message(&bot, target, response, get_keyboard_for_field(next_field)).await?;
|
app.bot
|
||||||
|
.send_html_message(target, response, get_keyboard_for_field(next_field))
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
// Final step - go to confirmation
|
// Final step - go to confirmation
|
||||||
enter_confirm_save_listing(&bot, dialogue, target, draft, None).await?;
|
enter_confirm_save_listing(app, dialogue, target, draft, None).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle text input for field editing
|
/// Handle text input for field editing
|
||||||
pub async fn handle_editing_field_input(
|
pub async fn handle_editing_field_input(
|
||||||
bot: Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
(field, mut draft): (ListingField, ListingDraft),
|
(field, mut draft): (ListingField, ListingDraft),
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
@@ -130,41 +132,32 @@ pub async fn handle_editing_field_input(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let flash = get_edit_success_message(field, draft.listing_type());
|
let flash = get_edit_success_message(field, draft.listing_type());
|
||||||
enter_edit_listing_draft(&bot, target, draft, dialogue, Some(flash)).await?;
|
enter_edit_listing_draft(app, target, draft, dialogue, Some(flash)).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle viewing draft confirmation callbacks
|
/// Handle viewing draft confirmation callbacks
|
||||||
pub async fn handle_viewing_draft_callback(
|
pub async fn handle_viewing_draft_callback(
|
||||||
listing_dao: ListingDAO,
|
app: App,
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
draft: ListingDraft,
|
draft: ListingDraft,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
let data = extract_callback_data(&bot, callback_query).await?;
|
let data = extract_callback_data(&app.bot, callback_query).await?;
|
||||||
|
|
||||||
match ConfirmationKeyboardButtons::try_from(data.as_str())? {
|
match ConfirmationKeyboardButtons::try_from(data.as_str())? {
|
||||||
ConfirmationKeyboardButtons::Create | ConfirmationKeyboardButtons::Save => {
|
ConfirmationKeyboardButtons::Create | ConfirmationKeyboardButtons::Save => {
|
||||||
info!("User {target:?} confirmed listing creation");
|
info!("User {target:?} confirmed listing creation");
|
||||||
let success_message = save_listing(&listing_dao, draft).await?;
|
let success_message = save_listing(&app.daos.listing, draft).await?;
|
||||||
enter_my_listings(
|
enter_my_listings(app, dialogue, user, target, Some(success_message)).await?;
|
||||||
listing_dao,
|
|
||||||
bot,
|
|
||||||
dialogue,
|
|
||||||
user,
|
|
||||||
target,
|
|
||||||
Some(success_message),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
ConfirmationKeyboardButtons::Cancel => {
|
ConfirmationKeyboardButtons::Cancel => {
|
||||||
info!("User {target:?} cancelled listing update");
|
info!("User {target:?} cancelled listing update");
|
||||||
let response = "🗑️ <b>Changes Discarded</b>\n\n\
|
let response = "🗑️ <b>Changes Discarded</b>\n\n\
|
||||||
Your changes have been discarded and not saved.";
|
Your changes have been discarded and not saved.";
|
||||||
send_message(&bot, target, &response, None).await?;
|
app.bot.send_html_message(target, &response, None).await?;
|
||||||
dialogue.exit().await.context("failed to exit dialogue")?;
|
dialogue.exit().await.context("failed to exit dialogue")?;
|
||||||
}
|
}
|
||||||
ConfirmationKeyboardButtons::Discard => {
|
ConfirmationKeyboardButtons::Discard => {
|
||||||
@@ -173,12 +166,12 @@ pub async fn handle_viewing_draft_callback(
|
|||||||
let response = "🗑️ <b>Listing Discarded</b>\n\n\
|
let response = "🗑️ <b>Listing Discarded</b>\n\n\
|
||||||
Your listing has been discarded and not created.\n\
|
Your listing has been discarded and not created.\n\
|
||||||
You can start a new listing anytime with /newlisting.";
|
You can start a new listing anytime with /newlisting.";
|
||||||
send_message(&bot, target, &response, None).await?;
|
app.bot.send_html_message(target, &response, None).await?;
|
||||||
dialogue.exit().await.context("failed to exit dialogue")?;
|
dialogue.exit().await.context("failed to exit dialogue")?;
|
||||||
}
|
}
|
||||||
ConfirmationKeyboardButtons::Edit => {
|
ConfirmationKeyboardButtons::Edit => {
|
||||||
info!("User {target:?} chose to edit listing");
|
info!("User {target:?} chose to edit listing");
|
||||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
enter_edit_listing_draft(app, target, draft, dialogue, None).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,18 +180,18 @@ pub async fn handle_viewing_draft_callback(
|
|||||||
|
|
||||||
/// Handle editing draft field selection callbacks
|
/// Handle editing draft field selection callbacks
|
||||||
pub async fn handle_editing_draft_callback(
|
pub async fn handle_editing_draft_callback(
|
||||||
bot: Bot,
|
app: App,
|
||||||
draft: ListingDraft,
|
draft: ListingDraft,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
let data = extract_callback_data(&bot, callback_query).await?;
|
let data = extract_callback_data(&app.bot, callback_query).await?;
|
||||||
info!("User {target:?} in editing screen, showing field selection");
|
info!("User {target:?} in editing screen, showing field selection");
|
||||||
|
|
||||||
let button = FieldSelectionKeyboardButtons::try_from(data.as_str())?;
|
let button = FieldSelectionKeyboardButtons::try_from(data.as_str())?;
|
||||||
if button == FieldSelectionKeyboardButtons::Done {
|
if button == FieldSelectionKeyboardButtons::Done {
|
||||||
return enter_confirm_save_listing(&bot, dialogue, target, draft, None).await;
|
return enter_confirm_save_listing(app, dialogue, target, draft, None).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let field = match button {
|
let field = match button {
|
||||||
@@ -224,44 +217,46 @@ pub async fn handle_editing_draft_callback(
|
|||||||
.context("failed to update dialogue")?;
|
.context("failed to update dialogue")?;
|
||||||
|
|
||||||
let response = format!("Editing {field:?}\n\nPrevious value: {value}");
|
let response = format!("Editing {field:?}\n\nPrevious value: {value}");
|
||||||
send_message(&bot, target, response, Some(keyboard)).await?;
|
app.bot
|
||||||
|
.send_html_message(target, response, Some(keyboard))
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle editing draft field callbacks (back button, etc.)
|
/// Handle editing draft field callbacks (back button, etc.)
|
||||||
pub async fn handle_editing_draft_field_callback(
|
pub async fn handle_editing_draft_field_callback(
|
||||||
bot: Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
(field, draft): (ListingField, ListingDraft),
|
(field, draft): (ListingField, ListingDraft),
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
let data = extract_callback_data(&bot, callback_query).await?;
|
let data = extract_callback_data(&app.bot, callback_query).await?;
|
||||||
info!("User {target:?} editing field: {field:?} -> {data:?}");
|
info!("User {target:?} editing field: {field:?} -> {data:?}");
|
||||||
|
|
||||||
if data == "edit_back" {
|
if data == "edit_back" {
|
||||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
enter_edit_listing_draft(app, target, draft, dialogue, None).await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// This callback handler typically receives button presses, not text input
|
// 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
|
// For now, just redirect back to edit screen since callback data isn't suitable for validation
|
||||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
enter_edit_listing_draft(app, target, draft, dialogue, None).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enter the edit listing draft screen
|
/// Enter the edit listing draft screen
|
||||||
pub async fn enter_edit_listing_draft(
|
pub async fn enter_edit_listing_draft(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
draft: ListingDraft,
|
draft: ListingDraft,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
flash_message: Option<String>,
|
flash_message: Option<String>,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
display_listing_summary(
|
display_listing_summary(
|
||||||
bot,
|
app,
|
||||||
target,
|
target,
|
||||||
&draft,
|
&draft,
|
||||||
Some(FieldSelectionKeyboardButtons::to_keyboard()),
|
Some(FieldSelectionKeyboardButtons::to_keyboard()),
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ use crate::commands::new_listing::keyboard::ConfirmationKeyboardButtons;
|
|||||||
use crate::commands::new_listing::messages::steps_for_listing_type;
|
use crate::commands::new_listing::messages::steps_for_listing_type;
|
||||||
use crate::commands::new_listing::NewListingState;
|
use crate::commands::new_listing::NewListingState;
|
||||||
use crate::db::ListingType;
|
use crate::db::ListingType;
|
||||||
|
use crate::App;
|
||||||
use crate::RootDialogue;
|
use crate::RootDialogue;
|
||||||
use crate::{commands::new_listing::types::ListingDraft, message_utils::*, BotResult};
|
use crate::{commands::new_listing::types::ListingDraft, message_utils::*, BotResult};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use teloxide::{types::InlineKeyboardMarkup, Bot};
|
use teloxide::types::InlineKeyboardMarkup;
|
||||||
|
|
||||||
/// Display the listing summary with optional flash message and keyboard
|
/// Display the listing summary with optional flash message and keyboard
|
||||||
pub async fn display_listing_summary(
|
pub async fn display_listing_summary(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
draft: &ListingDraft,
|
draft: &ListingDraft,
|
||||||
keyboard: Option<InlineKeyboardMarkup>,
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
@@ -48,14 +49,16 @@ pub async fn display_listing_summary(
|
|||||||
response_lines.push("".to_string());
|
response_lines.push("".to_string());
|
||||||
response_lines.push("Edit your listing:".to_string());
|
response_lines.push("Edit your listing:".to_string());
|
||||||
|
|
||||||
send_message(bot, target, response_lines.join("\n"), keyboard).await?;
|
app.bot
|
||||||
|
.send_html_message(target, response_lines.join("\n"), keyboard)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show the final confirmation screen before creating/saving the listing
|
/// Show the final confirmation screen before creating/saving the listing
|
||||||
pub async fn enter_confirm_save_listing(
|
pub async fn enter_confirm_save_listing(
|
||||||
bot: &Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
draft: ListingDraft,
|
draft: ListingDraft,
|
||||||
@@ -75,7 +78,7 @@ pub async fn enter_confirm_save_listing(
|
|||||||
])
|
])
|
||||||
};
|
};
|
||||||
|
|
||||||
display_listing_summary(bot, target, &draft, Some(keyboard), flash).await?;
|
display_listing_summary(app, target, &draft, Some(keyboard), flash).await?;
|
||||||
dialogue
|
dialogue
|
||||||
.update(NewListingState::ViewingDraft(draft))
|
.update(NewListingState::ViewingDraft(draft))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
message_utils::{send_message, MessageTarget},
|
message_utils::{MessageTarget, SendHtmlMessage},
|
||||||
BotResult,
|
App, BotResult,
|
||||||
};
|
};
|
||||||
use log::info;
|
use log::info;
|
||||||
use teloxide::{types::Message, Bot};
|
use teloxide::types::Message;
|
||||||
|
|
||||||
pub async fn handle_settings(bot: Bot, msg: Message, target: MessageTarget) -> BotResult {
|
pub async fn handle_settings(app: App, msg: Message, target: MessageTarget) -> BotResult {
|
||||||
let response = "⚙️ Settings (Coming Soon)\n\n\
|
let response = "⚙️ Settings (Coming Soon)\n\n\
|
||||||
Here you'll be able to configure:\n\
|
Here you'll be able to configure:\n\
|
||||||
• Notification preferences\n\
|
• Notification preferences\n\
|
||||||
@@ -20,6 +20,6 @@ pub async fn handle_settings(bot: Bot, msg: Message, target: MessageTarget) -> B
|
|||||||
msg.chat.id
|
msg.chat.id
|
||||||
);
|
);
|
||||||
|
|
||||||
send_message(&bot, target, response, None).await?;
|
app.bot.send_html_message(target, response, None).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ use log::info;
|
|||||||
use teloxide::{
|
use teloxide::{
|
||||||
types::{CallbackQuery, Update},
|
types::{CallbackQuery, Update},
|
||||||
utils::command::BotCommands,
|
utils::command::BotCommands,
|
||||||
Bot,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::my_listings::enter_my_listings,
|
commands::my_listings::enter_my_listings,
|
||||||
db::{user::PersistedUser, ListingDAO},
|
db::user::PersistedUser,
|
||||||
keyboard_buttons,
|
keyboard_buttons,
|
||||||
message_utils::{extract_callback_data, send_message, MessageTarget},
|
message_utils::{extract_callback_data, MessageTarget, SendHtmlMessage as _},
|
||||||
BotResult, Command, DialogueRootState, RootDialogue,
|
App, BotResult, Command, DialogueRootState, RootDialogue,
|
||||||
};
|
};
|
||||||
|
|
||||||
keyboard_buttons! {
|
keyboard_buttons! {
|
||||||
@@ -39,80 +38,79 @@ fn get_main_menu_message() -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_start(
|
pub async fn handle_start(
|
||||||
bot: Bot,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
update: Update,
|
update: Update,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
info!("got start message: {update:?}");
|
info!("got start message: {update:?}");
|
||||||
enter_main_menu(bot, dialogue, target).await?;
|
enter_main_menu(app, dialogue, target).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show the main menu with buttons
|
/// Show the main menu with buttons
|
||||||
pub async fn enter_main_menu(bot: Bot, dialogue: RootDialogue, target: MessageTarget) -> BotResult {
|
pub async fn enter_main_menu(app: App, dialogue: RootDialogue, target: MessageTarget) -> BotResult {
|
||||||
dialogue
|
dialogue
|
||||||
.update(DialogueRootState::MainMenu)
|
.update(DialogueRootState::MainMenu)
|
||||||
.await
|
.await
|
||||||
.context("failed to update dialogue")?;
|
.context("failed to update dialogue")?;
|
||||||
|
|
||||||
send_message(
|
app.bot
|
||||||
&bot,
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
get_main_menu_message(),
|
get_main_menu_message(),
|
||||||
Some(MainMenuButtons::to_keyboard()),
|
Some(MainMenuButtons::to_keyboard()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_main_menu_callback(
|
pub async fn handle_main_menu_callback(
|
||||||
listing_dao: ListingDAO,
|
app: App,
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
let data = extract_callback_data(&bot, callback_query).await?;
|
let data = extract_callback_data(&app.bot, callback_query).await?;
|
||||||
info!("User {target:?} selected main menu option: {data:?}");
|
info!("User {target:?} selected main menu option: {data:?}");
|
||||||
|
|
||||||
let button = MainMenuButtons::try_from(data.as_str())?;
|
let button = MainMenuButtons::try_from(data.as_str())?;
|
||||||
match button {
|
match button {
|
||||||
MainMenuButtons::MyListings => {
|
MainMenuButtons::MyListings => {
|
||||||
// Call show_listings_for_user directly
|
// Call show_listings_for_user directly
|
||||||
enter_my_listings(listing_dao, bot, dialogue, user, target, None).await?;
|
enter_my_listings(app, dialogue, user, target, None).await?;
|
||||||
}
|
}
|
||||||
MainMenuButtons::MyBids => {
|
MainMenuButtons::MyBids => {
|
||||||
send_message(
|
app.bot
|
||||||
&bot,
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
"💰 <b>My Bids (Coming Soon)</b>\n\n\
|
"💰 <b>My Bids (Coming Soon)</b>\n\n\
|
||||||
Here you'll be able to view:\n\
|
Here you'll be able to view:\n\
|
||||||
• Your active bids\n\
|
• Your active bids\n\
|
||||||
• Bid history\n\
|
• Bid history\n\
|
||||||
• Won/lost auctions\n\
|
• Won/lost auctions\n\
|
||||||
• Outbid notifications\n\n\
|
• Outbid notifications\n\n\
|
||||||
Feature in development! 🛠️",
|
Feature in development! 🛠️",
|
||||||
Some(MainMenuButtons::to_keyboard()),
|
Some(MainMenuButtons::to_keyboard()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
MainMenuButtons::Settings => {
|
MainMenuButtons::Settings => {
|
||||||
send_message(
|
app.bot
|
||||||
&bot,
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
"⚙️ <b>Settings (Coming Soon)</b>\n\n\
|
"⚙️ <b>Settings (Coming Soon)</b>\n\n\
|
||||||
Here you'll be able to configure:\n\
|
Here you'll be able to configure:\n\
|
||||||
• Notification preferences\n\
|
• Notification preferences\n\
|
||||||
• Language settings\n\
|
• Language settings\n\
|
||||||
• Default bid increments\n\
|
• Default bid increments\n\
|
||||||
• Outbid alerts\n\n\
|
• Outbid alerts\n\n\
|
||||||
Feature in development! 🛠️",
|
Feature in development! 🛠️",
|
||||||
Some(MainMenuButtons::to_keyboard()),
|
Some(MainMenuButtons::to_keyboard()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
MainMenuButtons::Help => {
|
MainMenuButtons::Help => {
|
||||||
let help_message = format!(
|
let help_message = format!(
|
||||||
@@ -121,13 +119,9 @@ pub async fn handle_main_menu_callback(
|
|||||||
🔗 <b>More info:</b> Use individual commands to get started!",
|
🔗 <b>More info:</b> Use individual commands to get started!",
|
||||||
Command::descriptions()
|
Command::descriptions()
|
||||||
);
|
);
|
||||||
send_message(
|
app.bot
|
||||||
&bot,
|
.send_html_message(target, help_message, Some(MainMenuButtons::to_keyboard()))
|
||||||
target,
|
.await?;
|
||||||
help_message,
|
|
||||||
Some(MainMenuButtons::to_keyboard()),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
188
src/db/dao/bid_dao.rs
Normal file
188
src/db/dao/bid_dao.rs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
use crate::db::{
|
||||||
|
bid::{NewBid, PersistedBid, PersistedBidFields},
|
||||||
|
bind_fields::BindFields,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::Utc;
|
||||||
|
use itertools::Itertools as _;
|
||||||
|
use sqlx::{prelude::*, sqlite::SqliteRow, SqlitePool};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BidDAO(SqlitePool);
|
||||||
|
|
||||||
|
impl BidDAO {
|
||||||
|
pub fn new(pool: SqlitePool) -> Self {
|
||||||
|
Self(pool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl BidDAO {
|
||||||
|
pub async fn insert_bid(&self, bid: NewBid) -> Result<PersistedBid> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let binds = BindFields::default()
|
||||||
|
.push("listing_id", &bid.listing_id)
|
||||||
|
.push("buyer_id", &bid.buyer_id)
|
||||||
|
.push("bid_amount", &bid.bid_amount)
|
||||||
|
.push("description", &bid.description)
|
||||||
|
.push("is_cancelled", &bid.is_cancelled)
|
||||||
|
.push("slot_number", &bid.slot_number)
|
||||||
|
.push("proxy_bid_id", &bid.proxy_bid_id)
|
||||||
|
.push("created_at", &now)
|
||||||
|
.push("updated_at", &now);
|
||||||
|
|
||||||
|
let query_str = format!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO bids ({}) VALUES ({})
|
||||||
|
RETURNING *
|
||||||
|
"#,
|
||||||
|
binds.bind_names().join(", "),
|
||||||
|
binds.bind_placeholders().join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
let row = binds
|
||||||
|
.bind_to_query(sqlx::query(&query_str))
|
||||||
|
.fetch_one(&self.0)
|
||||||
|
.await?;
|
||||||
|
Ok(FromRow::from_row(&row)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRow<'_, SqliteRow> for PersistedBid {
|
||||||
|
fn from_row(row: &'_ SqliteRow) -> std::result::Result<Self, sqlx::Error> {
|
||||||
|
Ok(PersistedBid {
|
||||||
|
persisted: PersistedBidFields::from_row(row)?,
|
||||||
|
listing_id: row.get("listing_id"),
|
||||||
|
buyer_id: row.get("buyer_id"),
|
||||||
|
bid_amount: row.get("bid_amount"),
|
||||||
|
description: row.get("description"),
|
||||||
|
is_cancelled: row.get("is_cancelled"),
|
||||||
|
slot_number: row.get("slot_number"),
|
||||||
|
proxy_bid_id: row.get("proxy_bid_id"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRow<'_, SqliteRow> for PersistedBidFields {
|
||||||
|
fn from_row(row: &'_ SqliteRow) -> std::result::Result<Self, sqlx::Error> {
|
||||||
|
Ok(PersistedBidFields {
|
||||||
|
id: row.get("id"),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
updated_at: row.get("updated_at"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::db::{
|
||||||
|
listing::{BasicAuctionFields, ListingFields},
|
||||||
|
models::{listing::NewListing, user::NewUser},
|
||||||
|
CurrencyType, ListingDAO, MoneyAmount, UserDAO,
|
||||||
|
};
|
||||||
|
use crate::test_utils::create_test_pool;
|
||||||
|
use chrono::Utc;
|
||||||
|
use teloxide::types::UserId;
|
||||||
|
|
||||||
|
async fn create_test_user_and_listing() -> (
|
||||||
|
UserDAO,
|
||||||
|
ListingDAO,
|
||||||
|
BidDAO,
|
||||||
|
crate::db::UserDbId,
|
||||||
|
crate::db::ListingDbId,
|
||||||
|
) {
|
||||||
|
let pool = create_test_pool().await;
|
||||||
|
let user_dao = UserDAO::new(pool.clone());
|
||||||
|
let listing_dao = ListingDAO::new(pool.clone());
|
||||||
|
let bid_dao = BidDAO::new(pool);
|
||||||
|
|
||||||
|
// Create a test user
|
||||||
|
let new_user = NewUser {
|
||||||
|
persisted: (),
|
||||||
|
telegram_id: UserId(12345).into(),
|
||||||
|
first_name: "Test User".to_string(),
|
||||||
|
last_name: None,
|
||||||
|
username: Some("testuser".to_string()),
|
||||||
|
is_banned: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = user_dao
|
||||||
|
.insert_user(&new_user)
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert test user");
|
||||||
|
|
||||||
|
// Create a test listing
|
||||||
|
let new_listing = NewListing {
|
||||||
|
persisted: (),
|
||||||
|
base: crate::db::listing::ListingBase {
|
||||||
|
seller_id: user.persisted.id,
|
||||||
|
title: "Test Listing".to_string(),
|
||||||
|
description: Some("Test description".to_string()),
|
||||||
|
currency_type: CurrencyType::Usd,
|
||||||
|
starts_at: Utc::now(),
|
||||||
|
ends_at: Utc::now() + chrono::Duration::hours(24),
|
||||||
|
},
|
||||||
|
fields: ListingFields::BasicAuction(BasicAuctionFields {
|
||||||
|
starting_bid: MoneyAmount::from_str("10.00").unwrap(),
|
||||||
|
buy_now_price: Some(MoneyAmount::from_str("100.00").unwrap()),
|
||||||
|
min_increment: MoneyAmount::from_str("1.00").unwrap(),
|
||||||
|
anti_snipe_minutes: Some(5),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let listing = listing_dao
|
||||||
|
.insert_listing(new_listing)
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert test listing");
|
||||||
|
|
||||||
|
(
|
||||||
|
user_dao,
|
||||||
|
listing_dao,
|
||||||
|
bid_dao,
|
||||||
|
user.persisted.id,
|
||||||
|
listing.persisted.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_insert_bid() {
|
||||||
|
let (_user_dao, _listing_dao, bid_dao, user_id, listing_id) =
|
||||||
|
create_test_user_and_listing().await;
|
||||||
|
|
||||||
|
let new_bid = NewBid {
|
||||||
|
persisted: (),
|
||||||
|
listing_id,
|
||||||
|
buyer_id: user_id,
|
||||||
|
bid_amount: MoneyAmount::from_str("25.50").unwrap(),
|
||||||
|
description: Some("Test bid description".to_string()),
|
||||||
|
is_cancelled: false,
|
||||||
|
slot_number: Some(1),
|
||||||
|
proxy_bid_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert bid
|
||||||
|
let inserted_bid = bid_dao
|
||||||
|
.insert_bid(new_bid.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert bid");
|
||||||
|
|
||||||
|
// Verify the inserted bid has the correct values
|
||||||
|
assert_eq!(inserted_bid.listing_id, new_bid.listing_id);
|
||||||
|
assert_eq!(inserted_bid.buyer_id, new_bid.buyer_id);
|
||||||
|
assert_eq!(inserted_bid.bid_amount, new_bid.bid_amount);
|
||||||
|
assert_eq!(inserted_bid.description, new_bid.description);
|
||||||
|
assert_eq!(inserted_bid.is_cancelled, new_bid.is_cancelled);
|
||||||
|
assert_eq!(inserted_bid.slot_number, new_bid.slot_number);
|
||||||
|
assert_eq!(inserted_bid.proxy_bid_id, new_bid.proxy_bid_id);
|
||||||
|
|
||||||
|
// Verify persisted fields are populated
|
||||||
|
assert!(inserted_bid.persisted.id.get() > 0);
|
||||||
|
assert!(inserted_bid.persisted.created_at <= chrono::Utc::now());
|
||||||
|
assert!(inserted_bid.persisted.updated_at <= chrono::Utc::now());
|
||||||
|
assert_eq!(
|
||||||
|
inserted_bid.persisted.created_at,
|
||||||
|
inserted_bid.persisted.updated_at
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,24 +22,6 @@ use crate::db::{
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ListingDAO(SqlitePool);
|
pub struct ListingDAO(SqlitePool);
|
||||||
|
|
||||||
const LISTING_RETURN_FIELDS: &[&str] = &[
|
|
||||||
"id",
|
|
||||||
"seller_id",
|
|
||||||
"listing_type",
|
|
||||||
"title",
|
|
||||||
"description",
|
|
||||||
"currency_type",
|
|
||||||
"starts_at",
|
|
||||||
"ends_at",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"starting_bid",
|
|
||||||
"buy_now_price",
|
|
||||||
"min_increment",
|
|
||||||
"anti_snipe_minutes",
|
|
||||||
"slots_available",
|
|
||||||
];
|
|
||||||
|
|
||||||
impl ListingDAO {
|
impl ListingDAO {
|
||||||
pub fn new(pool: SqlitePool) -> Self {
|
pub fn new(pool: SqlitePool) -> Self {
|
||||||
Self(pool)
|
Self(pool)
|
||||||
@@ -59,11 +41,10 @@ impl ListingDAO {
|
|||||||
let query_str = format!(
|
let query_str = format!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO listings ({}) VALUES ({})
|
INSERT INTO listings ({}) VALUES ({})
|
||||||
RETURNING {}
|
RETURNING *
|
||||||
"#,
|
"#,
|
||||||
binds.bind_names().join(", "),
|
binds.bind_names().join(", "),
|
||||||
binds.bind_placeholders().join(", "),
|
binds.bind_placeholders().join(", "),
|
||||||
LISTING_RETURN_FIELDS.join(", ")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let row = binds
|
let row = binds
|
||||||
@@ -83,13 +64,12 @@ impl ListingDAO {
|
|||||||
SET {}
|
SET {}
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
AND seller_id = ?
|
AND seller_id = ?
|
||||||
RETURNING {}
|
RETURNING *
|
||||||
"#,
|
"#,
|
||||||
binds
|
binds
|
||||||
.bind_names()
|
.bind_names()
|
||||||
.map(|name| format!("{name} = ?"))
|
.map(|name| format!("{name} = ?"))
|
||||||
.join(", "),
|
.join(", "),
|
||||||
LISTING_RETURN_FIELDS.join(", ")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let row = binds
|
let row = binds
|
||||||
@@ -103,26 +83,21 @@ impl ListingDAO {
|
|||||||
|
|
||||||
/// Find a listing by its ID
|
/// Find a listing by its ID
|
||||||
pub async fn find_by_id(&self, listing_id: ListingDbId) -> Result<Option<PersistedListing>> {
|
pub async fn find_by_id(&self, listing_id: ListingDbId) -> Result<Option<PersistedListing>> {
|
||||||
let result = sqlx::query_as(&format!(
|
let result = sqlx::query_as("SELECT * FROM listings WHERE id = ?")
|
||||||
"SELECT {} FROM listings WHERE id = ?",
|
.bind(listing_id)
|
||||||
LISTING_RETURN_FIELDS.join(", ")
|
.fetch_optional(&self.0)
|
||||||
))
|
.await?;
|
||||||
.bind(listing_id)
|
|
||||||
.fetch_optional(&self.0)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find all listings by a seller
|
/// Find all listings by a seller
|
||||||
pub async fn find_by_seller(&self, seller_id: UserDbId) -> Result<Vec<PersistedListing>> {
|
pub async fn find_by_seller(&self, seller_id: UserDbId) -> Result<Vec<PersistedListing>> {
|
||||||
let rows = sqlx::query_as(&format!(
|
let rows =
|
||||||
"SELECT {} FROM listings WHERE seller_id = ? ORDER BY created_at DESC",
|
sqlx::query_as("SELECT * FROM listings WHERE seller_id = ? ORDER BY created_at DESC")
|
||||||
LISTING_RETURN_FIELDS.join(", ")
|
.bind(seller_id)
|
||||||
))
|
.fetch_all(&self.0)
|
||||||
.bind(seller_id)
|
.await?;
|
||||||
.fetch_all(&self.0)
|
|
||||||
.await?;
|
|
||||||
Ok(rows)
|
Ok(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,26 @@
|
|||||||
pub mod listing_dao;
|
mod bid_dao;
|
||||||
pub mod user_dao;
|
mod listing_dao;
|
||||||
|
mod user_dao;
|
||||||
|
|
||||||
// Re-export DAO structs for easy access
|
// Re-export DAO structs for easy access
|
||||||
|
pub use bid_dao::BidDAO;
|
||||||
pub use listing_dao::ListingDAO;
|
pub use listing_dao::ListingDAO;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
pub use user_dao::UserDAO;
|
pub use user_dao::UserDAO;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DAOs {
|
||||||
|
pub user: UserDAO,
|
||||||
|
pub listing: ListingDAO,
|
||||||
|
pub bid: BidDAO,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DAOs {
|
||||||
|
pub fn new(pool: SqlitePool) -> Self {
|
||||||
|
Self {
|
||||||
|
user: UserDAO::new(pool.clone()),
|
||||||
|
listing: ListingDAO::new(pool.clone()),
|
||||||
|
bid: BidDAO::new(pool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,17 +17,6 @@ use crate::db::{
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UserDAO(SqlitePool);
|
pub struct UserDAO(SqlitePool);
|
||||||
|
|
||||||
const USER_RETURN_FIELDS: &[&str] = &[
|
|
||||||
"id",
|
|
||||||
"telegram_id",
|
|
||||||
"username",
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"is_banned",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
];
|
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
impl UserDAO {
|
impl UserDAO {
|
||||||
pub fn new(pool: SqlitePool) -> Self {
|
pub fn new(pool: SqlitePool) -> Self {
|
||||||
@@ -46,11 +35,10 @@ impl UserDAO {
|
|||||||
r#"
|
r#"
|
||||||
INSERT INTO users ({})
|
INSERT INTO users ({})
|
||||||
VALUES ({})
|
VALUES ({})
|
||||||
RETURNING {}
|
RETURNING *
|
||||||
"#,
|
"#,
|
||||||
binds.bind_names().join(", "),
|
binds.bind_names().join(", "),
|
||||||
binds.bind_placeholders().join(", "),
|
binds.bind_placeholders().join(", "),
|
||||||
USER_RETURN_FIELDS.join(", ")
|
|
||||||
);
|
);
|
||||||
let query = sqlx::query(&query_str);
|
let query = sqlx::query(&query_str);
|
||||||
let row = binds.bind_to_query(query).fetch_one(&self.0).await?;
|
let row = binds.bind_to_query(query).fetch_one(&self.0).await?;
|
||||||
@@ -60,16 +48,12 @@ impl UserDAO {
|
|||||||
|
|
||||||
/// Find a user by their ID
|
/// Find a user by their ID
|
||||||
pub async fn find_by_id(&self, user_id: UserDbId) -> Result<Option<PersistedUser>> {
|
pub async fn find_by_id(&self, user_id: UserDbId) -> Result<Option<PersistedUser>> {
|
||||||
Ok(sqlx::query_as::<_, PersistedUser>(
|
Ok(
|
||||||
r#"
|
sqlx::query_as::<_, PersistedUser>("SELECT * FROM users WHERE id = ? ")
|
||||||
SELECT id, telegram_id, first_name, last_name, username, is_banned, created_at, updated_at
|
.bind(user_id)
|
||||||
FROM users
|
.fetch_optional(&self.0)
|
||||||
WHERE id = ?
|
.await?,
|
||||||
"#,
|
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
|
||||||
.fetch_optional(&self.0)
|
|
||||||
.await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a user by their Telegram ID
|
/// Find a user by their Telegram ID
|
||||||
@@ -108,11 +92,10 @@ impl UserDAO {
|
|||||||
username = EXCLUDED.username,
|
username = EXCLUDED.username,
|
||||||
first_name = EXCLUDED.first_name,
|
first_name = EXCLUDED.first_name,
|
||||||
last_name = EXCLUDED.last_name
|
last_name = EXCLUDED.last_name
|
||||||
RETURNING {}
|
RETURNING *
|
||||||
"#,
|
"#,
|
||||||
binds.bind_names().join(", "),
|
binds.bind_names().join(", "),
|
||||||
binds.bind_placeholders().join(", "),
|
binds.bind_placeholders().join(", "),
|
||||||
USER_RETURN_FIELDS.join(", ")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let row = binds
|
let row = binds
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use sqlx::FromRow;
|
|
||||||
|
|
||||||
use crate::db::MoneyAmount;
|
use crate::db::{BidDbId, ListingDbId, MoneyAmount, ProxyBidDbId, UserDbId};
|
||||||
|
|
||||||
|
pub type PersistedBid = Bid<PersistedBidFields>;
|
||||||
|
#[allow(unused)]
|
||||||
|
pub type NewBid = Bid<()>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(unused)]
|
||||||
|
pub struct PersistedBidFields {
|
||||||
|
pub id: BidDbId,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Actual bids placed on listings
|
/// Actual bids placed on listings
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Debug, Clone, FromRow)]
|
pub struct Bid<P> {
|
||||||
pub struct Bid {
|
pub persisted: P,
|
||||||
pub id: i64,
|
pub listing_id: ListingDbId,
|
||||||
pub listing_id: i64,
|
pub buyer_id: UserDbId,
|
||||||
pub buyer_id: i64,
|
|
||||||
pub bid_amount: MoneyAmount,
|
pub bid_amount: MoneyAmount,
|
||||||
|
|
||||||
// For blind listings
|
// For blind listings
|
||||||
@@ -20,20 +31,24 @@ pub struct Bid {
|
|||||||
pub slot_number: Option<i32>, // For multi-slot listings
|
pub slot_number: Option<i32>, // For multi-slot listings
|
||||||
|
|
||||||
// Reference to proxy bid if auto-generated
|
// Reference to proxy bid if auto-generated
|
||||||
pub proxy_bid_id: Option<i64>,
|
pub proxy_bid_id: Option<ProxyBidDbId>,
|
||||||
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// New bid data for insertion
|
impl<P> Bid<P> {
|
||||||
#[allow(unused)]
|
pub fn new_basic(
|
||||||
#[derive(Debug, Clone)]
|
listing_id: ListingDbId,
|
||||||
pub struct NewBid {
|
buyer_id: UserDbId,
|
||||||
pub listing_id: i64,
|
bid_amount: MoneyAmount,
|
||||||
pub buyer_id: i64,
|
) -> NewBid {
|
||||||
pub bid_amount: MoneyAmount,
|
NewBid {
|
||||||
pub description: Option<String>,
|
persisted: (),
|
||||||
pub slot_number: Option<i32>,
|
listing_id,
|
||||||
pub proxy_bid_id: Option<i64>,
|
buyer_id,
|
||||||
|
bid_amount,
|
||||||
|
description: None,
|
||||||
|
is_cancelled: false,
|
||||||
|
slot_number: None,
|
||||||
|
proxy_bid_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ pub struct PersistedListingFields {
|
|||||||
|
|
||||||
/// Main listing/auction entity
|
/// Main listing/auction entity
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
#[allow(unused)]
|
|
||||||
pub struct Listing<P: Debug + Clone> {
|
pub struct Listing<P: Debug + Clone> {
|
||||||
pub persisted: P,
|
pub persisted: P,
|
||||||
pub base: ListingBase,
|
pub base: ListingBase,
|
||||||
@@ -49,7 +48,6 @@ impl<'a, P: Debug + Clone> From<&'a mut Listing<P>> for ListingBaseFieldsMut<'a>
|
|||||||
|
|
||||||
/// Common fields shared by all listing types
|
/// Common fields shared by all listing types
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[allow(unused)]
|
|
||||||
pub struct ListingBase {
|
pub struct ListingBase {
|
||||||
pub seller_id: UserDbId,
|
pub seller_id: UserDbId,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -72,7 +70,6 @@ impl ListingBase {
|
|||||||
|
|
||||||
/// Fields specific to basic auction listings
|
/// Fields specific to basic auction listings
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[allow(unused)]
|
|
||||||
pub struct BasicAuctionFields {
|
pub struct BasicAuctionFields {
|
||||||
pub starting_bid: MoneyAmount,
|
pub starting_bid: MoneyAmount,
|
||||||
pub buy_now_price: Option<MoneyAmount>,
|
pub buy_now_price: Option<MoneyAmount>,
|
||||||
@@ -82,7 +79,6 @@ pub struct BasicAuctionFields {
|
|||||||
|
|
||||||
/// Fields specific to multi-slot auction listings
|
/// Fields specific to multi-slot auction listings
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[allow(unused)]
|
|
||||||
pub struct MultiSlotAuctionFields {
|
pub struct MultiSlotAuctionFields {
|
||||||
pub starting_bid: MoneyAmount,
|
pub starting_bid: MoneyAmount,
|
||||||
pub buy_now_price: MoneyAmount,
|
pub buy_now_price: MoneyAmount,
|
||||||
@@ -93,7 +89,6 @@ pub struct MultiSlotAuctionFields {
|
|||||||
|
|
||||||
/// Fields specific to fixed price listings
|
/// Fields specific to fixed price listings
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[allow(unused)]
|
|
||||||
pub struct FixedPriceListingFields {
|
pub struct FixedPriceListingFields {
|
||||||
pub buy_now_price: MoneyAmount,
|
pub buy_now_price: MoneyAmount,
|
||||||
pub slots_available: i32,
|
pub slots_available: i32,
|
||||||
@@ -101,13 +96,11 @@ pub struct FixedPriceListingFields {
|
|||||||
|
|
||||||
/// Fields specific to blind auction listings
|
/// Fields specific to blind auction listings
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[allow(unused)]
|
|
||||||
pub struct BlindAuctionFields {
|
pub struct BlindAuctionFields {
|
||||||
pub starting_bid: MoneyAmount,
|
pub starting_bid: MoneyAmount,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[allow(unused)]
|
|
||||||
pub enum ListingFields {
|
pub enum ListingFields {
|
||||||
BasicAuction(BasicAuctionFields),
|
BasicAuction(BasicAuctionFields),
|
||||||
MultiSlotAuction(MultiSlotAuctionFields),
|
MultiSlotAuction(MultiSlotAuctionFields),
|
||||||
|
|||||||
93
src/db/types/db_id.rs
Normal file
93
src/db/types/db_id.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{
|
||||||
|
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||||
|
};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
macro_rules! impl_db_id {
|
||||||
|
($id_name:ident, $id_type:ty) => {
|
||||||
|
#[doc = "Type-safe wrapper for "]
|
||||||
|
#[doc = stringify!($id_name)]
|
||||||
|
#[doc = " IDs"]
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub struct $id_name($id_type);
|
||||||
|
|
||||||
|
impl $id_name {
|
||||||
|
/// Create a new ListingId from an i64
|
||||||
|
pub fn new(id: $id_type) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the inner i64 value
|
||||||
|
pub fn get(&self) -> $id_type {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$id_type> for $id_name {
|
||||||
|
fn from(id: $id_type) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$id_name> for $id_type {
|
||||||
|
fn from(value: $id_name) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for $id_name {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLx implementations for database compatibility
|
||||||
|
impl Type<Sqlite> for $id_name {
|
||||||
|
fn type_info() -> SqliteTypeInfo {
|
||||||
|
<$id_type as Type<Sqlite>>::type_info()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compatible(ty: &SqliteTypeInfo) -> bool {
|
||||||
|
<$id_type as Type<Sqlite>>::compatible(ty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'q> Encode<'q, Sqlite> for $id_name {
|
||||||
|
fn encode_by_ref(
|
||||||
|
&self,
|
||||||
|
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
|
||||||
|
) -> Result<IsNull, BoxDynError> {
|
||||||
|
<$id_type as Encode<'q, Sqlite>>::encode_by_ref(&self.0, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> Decode<'r, Sqlite> for $id_name {
|
||||||
|
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||||
|
let id = <$id_type as Decode<'r, Sqlite>>::decode(value)?;
|
||||||
|
Ok(Self(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_db_id!(BidDbId, i64);
|
||||||
|
impl_db_id!(ProxyBidDbId, i64);
|
||||||
|
impl_db_id!(ListingDbId, i64);
|
||||||
|
impl_db_id!(UserDbId, i64);
|
||||||
|
|
||||||
|
impl_db_id!(TelegramUserDbId, i64);
|
||||||
|
|
||||||
|
impl From<teloxide::types::UserId> for TelegramUserDbId {
|
||||||
|
fn from(id: teloxide::types::UserId) -> Self {
|
||||||
|
Self(id.0 as i64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TelegramUserDbId> for teloxide::types::UserId {
|
||||||
|
fn from(user_id: TelegramUserDbId) -> Self {
|
||||||
|
teloxide::types::UserId(user_id.0 as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
//! ListingId newtype for type-safe listing identification
|
|
||||||
//!
|
|
||||||
//! This newtype prevents accidentally mixing up listing IDs with other ID types
|
|
||||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::{
|
|
||||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
|
||||||
};
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
/// Type-safe wrapper for listing IDs
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
||||||
pub struct ListingDbId(i64);
|
|
||||||
|
|
||||||
impl ListingDbId {
|
|
||||||
/// Create a new ListingId from an i64
|
|
||||||
pub fn new(id: i64) -> Self {
|
|
||||||
Self(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the inner i64 value
|
|
||||||
pub fn get(&self) -> i64 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<i64> for ListingDbId {
|
|
||||||
fn from(id: i64) -> Self {
|
|
||||||
Self(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ListingDbId> for i64 {
|
|
||||||
fn from(listing_id: ListingDbId) -> Self {
|
|
||||||
listing_id.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ListingDbId {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQLx implementations for database compatibility
|
|
||||||
impl Type<Sqlite> for ListingDbId {
|
|
||||||
fn type_info() -> SqliteTypeInfo {
|
|
||||||
<i64 as Type<Sqlite>>::type_info()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compatible(ty: &SqliteTypeInfo) -> bool {
|
|
||||||
<i64 as Type<Sqlite>>::compatible(ty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'q> Encode<'q, Sqlite> for ListingDbId {
|
|
||||||
fn encode_by_ref(
|
|
||||||
&self,
|
|
||||||
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
|
|
||||||
) -> Result<IsNull, BoxDynError> {
|
|
||||||
<i64 as Encode<'q, Sqlite>>::encode_by_ref(&self.0, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'r> Decode<'r, Sqlite> for ListingDbId {
|
|
||||||
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
|
|
||||||
let id = <i64 as Decode<'r, Sqlite>>::decode(value)?;
|
|
||||||
Ok(Self(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
mod listing_db_id;
|
|
||||||
mod telegram_user_db_id;
|
|
||||||
mod user_db_id;
|
|
||||||
|
|
||||||
pub use listing_db_id::ListingDbId;
|
|
||||||
pub use telegram_user_db_id::TelegramUserDbId;
|
|
||||||
pub use user_db_id::UserDbId;
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
//! TelegramUserId
|
|
||||||
//! newtype for type-safe user identification
|
|
||||||
//!
|
|
||||||
//! This newtype prevents accidentally mixing up user IDs with other ID types
|
|
||||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
|
||||||
|
|
||||||
use sqlx::{
|
|
||||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
|
||||||
};
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
/// Type-safe wrapper for user IDs
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct TelegramUserDbId(teloxide::types::UserId);
|
|
||||||
|
|
||||||
impl TelegramUserDbId {
|
|
||||||
/// Create a new TelegramUserId
|
|
||||||
/// from an i64
|
|
||||||
pub fn new(id: teloxide::types::UserId) -> Self {
|
|
||||||
Self(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the inner i64 value
|
|
||||||
pub fn get(&self) -> teloxide::types::UserId {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<teloxide::types::UserId> for TelegramUserDbId {
|
|
||||||
fn from(id: teloxide::types::UserId) -> Self {
|
|
||||||
Self(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u64> for TelegramUserDbId {
|
|
||||||
fn from(id: u64) -> Self {
|
|
||||||
Self(teloxide::types::UserId(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<TelegramUserDbId> for teloxide::types::UserId {
|
|
||||||
fn from(user_id: TelegramUserDbId) -> Self {
|
|
||||||
user_id.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for TelegramUserDbId {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQLx implementations for database compatibility
|
|
||||||
impl Type<Sqlite> for TelegramUserDbId {
|
|
||||||
fn type_info() -> SqliteTypeInfo {
|
|
||||||
<i64 as Type<Sqlite>>::type_info()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compatible(ty: &SqliteTypeInfo) -> bool {
|
|
||||||
<i64 as Type<Sqlite>>::compatible(ty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'q> Encode<'q, Sqlite> for TelegramUserDbId {
|
|
||||||
fn encode_by_ref(
|
|
||||||
&self,
|
|
||||||
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
|
|
||||||
) -> Result<IsNull, BoxDynError> {
|
|
||||||
<i64 as Encode<'q, Sqlite>>::encode(self.0 .0 as i64, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'r> Decode<'r, Sqlite> for TelegramUserDbId {
|
|
||||||
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
|
|
||||||
let id = <i64 as Decode<'r, Sqlite>>::decode(value)?;
|
|
||||||
Ok(Self(teloxide::types::UserId(id as u64)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
//! UserId newtype for type-safe user identification
|
|
||||||
//!
|
|
||||||
//! This newtype prevents accidentally mixing up user IDs with other ID types
|
|
||||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::{
|
|
||||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
|
||||||
};
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
/// Type-safe wrapper for user IDs
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct UserDbId(i64);
|
|
||||||
|
|
||||||
impl UserDbId {
|
|
||||||
/// Create a new UserId from an i64
|
|
||||||
pub fn new(id: i64) -> Self {
|
|
||||||
Self(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the inner i64 value
|
|
||||||
pub fn get(&self) -> i64 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<i64> for UserDbId {
|
|
||||||
fn from(id: i64) -> Self {
|
|
||||||
Self(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<UserDbId> for i64 {
|
|
||||||
fn from(user_id: UserDbId) -> Self {
|
|
||||||
user_id.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for UserDbId {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQLx implementations for database compatibility
|
|
||||||
impl Type<Sqlite> for UserDbId {
|
|
||||||
fn type_info() -> SqliteTypeInfo {
|
|
||||||
<i64 as Type<Sqlite>>::type_info()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compatible(ty: &SqliteTypeInfo) -> bool {
|
|
||||||
<i64 as Type<Sqlite>>::compatible(ty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'q> Encode<'q, Sqlite> for UserDbId {
|
|
||||||
fn encode_by_ref(
|
|
||||||
&self,
|
|
||||||
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
|
|
||||||
) -> Result<IsNull, BoxDynError> {
|
|
||||||
<i64 as Encode<'q, Sqlite>>::encode_by_ref(&self.0, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'r> Decode<'r, Sqlite> for UserDbId {
|
|
||||||
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
|
|
||||||
let id = <i64 as Decode<'r, Sqlite>>::decode(value)?;
|
|
||||||
Ok(Self(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
use std::{collections::BTreeSet, ops::ControlFlow, sync::Arc};
|
||||||
|
|
||||||
|
use dptree::{
|
||||||
|
di::{Asyncify, Injectable},
|
||||||
|
from_fn_with_description, Handler, HandlerDescription, HandlerSignature, Type,
|
||||||
|
};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! case {
|
macro_rules! case {
|
||||||
// Basic variant matching without parameters
|
// Basic variant matching without parameters
|
||||||
@@ -72,6 +79,62 @@ macro_rules! case {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait MapTwo<'a, Output, Descr> {
|
||||||
|
#[must_use]
|
||||||
|
#[track_caller]
|
||||||
|
fn map2<Proj, NewType1, NewType2, Args>(self, proj: Proj) -> Handler<'a, Output, Descr>
|
||||||
|
where
|
||||||
|
Asyncify<Proj>: Injectable<(NewType1, NewType2), Args> + Send + Sync + 'a,
|
||||||
|
NewType1: Send + Sync + 'static,
|
||||||
|
NewType2: Send + Sync + 'static;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Output, Descr> MapTwo<'a, Output, Descr> for Handler<'a, Output, Descr>
|
||||||
|
where
|
||||||
|
Output: 'a,
|
||||||
|
Descr: HandlerDescription,
|
||||||
|
{
|
||||||
|
fn map2<Proj, NewType1, NewType2, Args>(self, proj: Proj) -> Handler<'a, Output, Descr>
|
||||||
|
where
|
||||||
|
Asyncify<Proj>: Injectable<(NewType1, NewType2), Args> + Send + Sync + 'a,
|
||||||
|
NewType1: Send + Sync + 'static,
|
||||||
|
NewType2: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let proj = Arc::new(Asyncify(proj));
|
||||||
|
|
||||||
|
self.chain(from_fn_with_description(
|
||||||
|
Descr::map(),
|
||||||
|
move |container, cont| {
|
||||||
|
let proj = Arc::clone(&proj);
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let proj = proj.inject(&container);
|
||||||
|
let (res1, res2) = proj().await;
|
||||||
|
std::mem::drop(proj);
|
||||||
|
|
||||||
|
let mut intermediate = container.clone();
|
||||||
|
intermediate.insert(res1);
|
||||||
|
intermediate.insert(res2);
|
||||||
|
match cont(intermediate).await {
|
||||||
|
ControlFlow::Continue(_) => ControlFlow::Continue(container),
|
||||||
|
ControlFlow::Break(result) => ControlFlow::Break(result),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
HandlerSignature::Other {
|
||||||
|
obligations:
|
||||||
|
<Asyncify<Proj> as Injectable<(NewType1, NewType2), Args>>::obligations(),
|
||||||
|
guaranteed_outcomes: BTreeSet::from_iter(vec![
|
||||||
|
Type::of::<NewType1>(),
|
||||||
|
Type::of::<NewType2>(),
|
||||||
|
]),
|
||||||
|
conditional_outcomes: BTreeSet::new(),
|
||||||
|
continues: true,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::ops::ControlFlow;
|
use std::ops::ControlFlow;
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
message_utils::{send_message, MessageTarget},
|
message_utils::{MessageTarget, SendHtmlMessage},
|
||||||
wrap_endpoint, BotError, BotResult, WrappedAsyncFn,
|
wrap_endpoint, BotError, BotResult, WrappedAsyncFn,
|
||||||
};
|
};
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use std::{future::Future, pin::Pin};
|
|
||||||
use teloxide::Bot;
|
use teloxide::Bot;
|
||||||
|
|
||||||
pub async fn handle_error(bot: Bot, target: MessageTarget, error: BotError) -> BotResult {
|
pub async fn handle_error(bot: Bot, target: MessageTarget, error: BotError) -> BotResult {
|
||||||
log::error!("Error in handler: {error:?}");
|
log::error!("Error in handler: {error:?}");
|
||||||
match error {
|
match error {
|
||||||
BotError::UserVisibleError(message) => send_message(&bot, target, message, None).await?,
|
BotError::UserVisibleError(message) => bot.send_html_message(target, message, None).await?,
|
||||||
BotError::InternalError(_) => {
|
BotError::InternalError(_) => {
|
||||||
send_message(
|
bot.send_html_message(
|
||||||
&bot,
|
|
||||||
target,
|
target,
|
||||||
"An internal error occurred. Please try again later.",
|
"An internal error occurred. Please try again later.",
|
||||||
None,
|
None,
|
||||||
@@ -27,7 +25,7 @@ fn boxed_handle_error(
|
|||||||
bot: Bot,
|
bot: Bot,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
error: BotError,
|
error: BotError,
|
||||||
) -> Pin<Box<dyn Future<Output = BotResult> + Send>> {
|
) -> BoxFuture<'static, BotResult> {
|
||||||
Box::pin(handle_error(bot, target, error))
|
Box::pin(handle_error(bot, target, error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
src/main.rs
22
src/main.rs
@@ -19,7 +19,7 @@ use crate::commands::{
|
|||||||
my_listings::{my_listings_handler, my_listings_inline_handler, MyListingsState},
|
my_listings::{my_listings_handler, my_listings_inline_handler, MyListingsState},
|
||||||
new_listing::{new_listing_handler, NewListingState},
|
new_listing::{new_listing_handler, NewListingState},
|
||||||
};
|
};
|
||||||
use crate::db::{ListingDAO, UserDAO};
|
use crate::db::DAOs;
|
||||||
use crate::handle_error::with_error_handler;
|
use crate::handle_error::with_error_handler;
|
||||||
use crate::handler_utils::{find_or_create_db_user_from_update, update_into_message_target};
|
use crate::handler_utils::{find_or_create_db_user_from_update, update_into_message_target};
|
||||||
use crate::sqlite_storage::SqliteStorage;
|
use crate::sqlite_storage::SqliteStorage;
|
||||||
@@ -33,6 +33,18 @@ use teloxide::dispatching::dialogue::serializer::Json;
|
|||||||
use teloxide::{prelude::*, types::BotCommand, utils::command::BotCommands};
|
use teloxide::{prelude::*, types::BotCommand, utils::command::BotCommands};
|
||||||
pub use wrap_endpoint::*;
|
pub use wrap_endpoint::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct App {
|
||||||
|
pub bot: Bot,
|
||||||
|
pub daos: DAOs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(bot: Bot, daos: DAOs) -> Self {
|
||||||
|
Self { bot, daos }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set up the bot's command menu that appears when users tap the menu button
|
/// Set up the bot's command menu that appears when users tap the menu button
|
||||||
async fn setup_bot_commands(bot: &Bot) -> Result<()> {
|
async fn setup_bot_commands(bot: &Bot) -> Result<()> {
|
||||||
info!("Setting up bot command menu...");
|
info!("Setting up bot command menu...");
|
||||||
@@ -94,6 +106,8 @@ async fn main() -> Result<()> {
|
|||||||
setup_bot_commands(&bot).await?;
|
setup_bot_commands(&bot).await?;
|
||||||
|
|
||||||
let dialog_storage = SqliteStorage::new(db_pool.clone(), Json).await?;
|
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
|
// Create dispatcher with dialogue system
|
||||||
Dispatcher::builder(
|
Dispatcher::builder(
|
||||||
@@ -137,11 +151,7 @@ async fn main() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.branch(Update::filter_message().endpoint(with_error_handler(unknown_message_handler))),
|
.branch(Update::filter_message().endpoint(with_error_handler(unknown_message_handler))),
|
||||||
)
|
)
|
||||||
.dependencies(dptree::deps![
|
.dependencies(dptree::deps![dialog_storage, daos, app])
|
||||||
dialog_storage,
|
|
||||||
ListingDAO::new(db_pool.clone()),
|
|
||||||
UserDAO::new(db_pool.clone())
|
|
||||||
])
|
|
||||||
.enable_ctrlc_handler()
|
.enable_ctrlc_handler()
|
||||||
.worker_queue_size(1)
|
.worker_queue_size(1)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -121,8 +121,28 @@ impl TryFrom<&CallbackQuery> for MessageTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait SendHtmlMessage {
|
||||||
|
async fn send_html_message(
|
||||||
|
&self,
|
||||||
|
target: MessageTarget,
|
||||||
|
text: impl AsRef<str>,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendHtmlMessage for Bot {
|
||||||
|
async fn send_html_message(
|
||||||
|
&self,
|
||||||
|
target: MessageTarget,
|
||||||
|
text: impl AsRef<str>,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult {
|
||||||
|
send_html_message(self, target, text, keyboard).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Unified HTML message sending utility
|
// Unified HTML message sending utility
|
||||||
pub async fn send_message(
|
async fn send_html_message(
|
||||||
bot: &Bot,
|
bot: &Bot,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
text: impl AsRef<str>,
|
text: impl AsRef<str>,
|
||||||
|
|||||||
@@ -68,32 +68,36 @@ macro_rules! generate_wrapped {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_wrapped!([], []);
|
macro_rules! generate_wrapped_all {
|
||||||
generate_wrapped!([T1], []);
|
// Entry: two lists (base generics, error generics)
|
||||||
generate_wrapped!([T1, T2], []);
|
([$($base:ident),*], [$($err:ident),*]) => {
|
||||||
generate_wrapped!([T1, T2, T3], []);
|
generate_wrapped_all!(@recurse_base [$($base),*] [$($err),*]);
|
||||||
generate_wrapped!([T1, T2, T3, T4], []);
|
};
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5], []);
|
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5, T6], []);
|
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5, T6, T7], []);
|
|
||||||
|
|
||||||
generate_wrapped!([], [E1]);
|
// Recurse over base prefixes (from full list down to empty)
|
||||||
generate_wrapped!([T1], [E1]);
|
(@recurse_base [] [$($err:ident),*]) => {
|
||||||
generate_wrapped!([T1, T2], [E1]);
|
generate_wrapped_all!(@recurse_err [] [$($err),*]);
|
||||||
generate_wrapped!([T1, T2, T3], [E1]);
|
};
|
||||||
generate_wrapped!([T1, T2, T3, T4], [E1]);
|
(@recurse_base [$head:ident $(, $tail:ident)*] [$($err:ident),*]) => {
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5], [E1]);
|
generate_wrapped_all!(@recurse_err [$head $(, $tail)*] [$($err),*]);
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5, T6], [E1]);
|
generate_wrapped_all!(@recurse_base [$( $tail ),*] [$($err),*]);
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5, T6, T7], [E1]);
|
};
|
||||||
|
|
||||||
generate_wrapped!([], [E1, E2]);
|
// For a fixed base prefix, recurse over error prefixes (from full list down to empty)
|
||||||
generate_wrapped!([T1], [E1, E2]);
|
(@recurse_err [$($base_current:ident),*] []) => {
|
||||||
generate_wrapped!([T1, T2], [E1, E2]);
|
generate_wrapped!([$($base_current),*], []);
|
||||||
generate_wrapped!([T1, T2, T3], [E1, E2]);
|
};
|
||||||
generate_wrapped!([T1, T2, T3, T4], [E1, E2]);
|
(@recurse_err [$($base_current:ident),*] [$ehead:ident $(, $etail:ident)*]) => {
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5], [E1, E2]);
|
generate_wrapped!([$($base_current),*], [$ehead $(, $etail)*]);
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5, T6], [E1, E2]);
|
generate_wrapped_all!(@recurse_err [$($base_current),*] [$( $etail ),*]);
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5, T6, T7], [E1, E2]);
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate cartesian product of prefixes up to 12 base generics and 2 error generics
|
||||||
|
generate_wrapped_all!(
|
||||||
|
[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12],
|
||||||
|
[E1, E2]
|
||||||
|
);
|
||||||
|
|
||||||
pub fn wrap_endpoint<FnBase, FnError, ErrorType, FnBaseArgs, FnErrorArgs>(
|
pub fn wrap_endpoint<FnBase, FnError, ErrorType, FnBaseArgs, FnErrorArgs>(
|
||||||
fn_base: FnBase,
|
fn_base: FnBase,
|
||||||
|
|||||||
Reference in New Issue
Block a user