basic scaffold for placing bids
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1616,6 +1616,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"dptree",
|
"dptree",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ regex = "1.11.2"
|
|||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
dptree = "0.5.1"
|
dptree = "0.5.1"
|
||||||
seq-macro = "0.3.6"
|
seq-macro = "0.3.6"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = "0.26.1"
|
rstest = "0.26.1"
|
||||||
|
|||||||
1
src/bidding/keyboards.rs
Normal file
1
src/bidding/keyboards.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
212
src/bidding/mod.rs
Normal file
212
src/bidding/mod.rs
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
mod keyboards;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
case,
|
||||||
|
commands::new_listing::validations::{validate_price, SetFieldError},
|
||||||
|
db::{
|
||||||
|
listing::{ListingFields, PersistedListing},
|
||||||
|
user::PersistedUser,
|
||||||
|
ListingDbId, MoneyAmount, UserDAO,
|
||||||
|
},
|
||||||
|
handle_error::with_error_handler,
|
||||||
|
handler_utils::find_listing_by_id,
|
||||||
|
message_utils::{send_message, MessageTarget},
|
||||||
|
start_command_data::StartCommandData,
|
||||||
|
BotError, BotHandler, BotResult, DialogueRootState, RootDialogue,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
use log::info;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use teloxide::{
|
||||||
|
dispatching::UpdateFilterExt,
|
||||||
|
types::{CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update},
|
||||||
|
Bot,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum BiddingState {
|
||||||
|
AwaitingBidAmount(ListingDbId),
|
||||||
|
AwaitingConfirmBidAmount(ListingDbId, MoneyAmount),
|
||||||
|
}
|
||||||
|
impl From<BiddingState> for DialogueRootState {
|
||||||
|
fn from(state: BiddingState) -> Self {
|
||||||
|
DialogueRootState::Bidding(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bidding_handler() -> BotHandler {
|
||||||
|
dptree::entry()
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.filter_map(StartCommandData::get_from_update)
|
||||||
|
.filter_map(StartCommandData::get_place_bid_on_listing_start_command)
|
||||||
|
.filter_map_async(find_listing_by_id)
|
||||||
|
.endpoint(with_error_handler(handle_place_bid_on_listing)),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.chain(case![DialogueRootState::Bidding(
|
||||||
|
BiddingState::AwaitingBidAmount(listing_id)
|
||||||
|
)])
|
||||||
|
.filter_map_async(find_listing_by_id)
|
||||||
|
.endpoint(with_error_handler(handle_awaiting_bid_amount_input)),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
Update::filter_callback_query()
|
||||||
|
.chain(case![DialogueRootState::Bidding(
|
||||||
|
BiddingState::AwaitingConfirmBidAmount(listing_id, bid_amount)
|
||||||
|
)])
|
||||||
|
.filter_map_async(
|
||||||
|
async |listing_dao, (listing_id, _): (ListingDbId, MoneyAmount)| {
|
||||||
|
find_listing_by_id(listing_dao, listing_id).await
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.endpoint(with_error_handler(
|
||||||
|
handle_awaiting_confirm_bid_amount_callback,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_place_bid_on_listing(
|
||||||
|
bot: Bot,
|
||||||
|
user_dao: UserDAO,
|
||||||
|
target: MessageTarget,
|
||||||
|
user: PersistedUser,
|
||||||
|
listing: PersistedListing,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
) -> BotResult {
|
||||||
|
info!("Handling place bid on listing for listing {listing:?} for user {user:?}");
|
||||||
|
let seller = user_dao
|
||||||
|
.find_by_id(listing.base.seller_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(BotError::UserVisibleError("Seller not found".to_string()))?;
|
||||||
|
|
||||||
|
let fields = match &listing.fields {
|
||||||
|
ListingFields::BasicAuction(fields) => fields,
|
||||||
|
_ => {
|
||||||
|
return Err(BotError::UserVisibleError(
|
||||||
|
"Unsupported listing type".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dialogue
|
||||||
|
.update(BiddingState::AwaitingBidAmount(listing.persisted.id))
|
||||||
|
.await
|
||||||
|
.context("failed to update dialogue")?;
|
||||||
|
|
||||||
|
let mut response_lines = vec![];
|
||||||
|
response_lines.push(format!(
|
||||||
|
"Place bid on listing for listing <b>{}</b>, ran by {}",
|
||||||
|
listing.base.title,
|
||||||
|
seller
|
||||||
|
.username
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| seller.telegram_id.to_string())
|
||||||
|
));
|
||||||
|
response_lines.push(format!("You are bidding on this listing as: {user:?}"));
|
||||||
|
response_lines.push(format!("Minimum bid: {}", fields.min_increment));
|
||||||
|
|
||||||
|
let keyboard = InlineKeyboardMarkup::default()
|
||||||
|
.append_row([InlineKeyboardButton::callback("Bid $1", "cancel")]);
|
||||||
|
|
||||||
|
send_message(&bot, target, response_lines.join("\n"), Some(keyboard)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_awaiting_bid_amount_input(
|
||||||
|
bot: Bot,
|
||||||
|
listing: PersistedListing,
|
||||||
|
target: MessageTarget,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
msg: Message,
|
||||||
|
) -> BotResult {
|
||||||
|
// parse the bid amount into a MoneyAmount
|
||||||
|
let text = msg
|
||||||
|
.text()
|
||||||
|
.ok_or(BotError::user_visible("Please enter a valid bid amount"))?;
|
||||||
|
let bid_amount = match validate_price(text) {
|
||||||
|
Ok(bid_amount) => bid_amount,
|
||||||
|
Err(SetFieldError::ValidationFailed(e)) => {
|
||||||
|
return Err(BotError::user_visible(e));
|
||||||
|
}
|
||||||
|
Err(other) => {
|
||||||
|
return Err(anyhow!("Error validating bid amount: {other:?}").into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
send_message(
|
||||||
|
&bot,
|
||||||
|
target,
|
||||||
|
format!("Confirm bid amount: {bid_amount} - this cannot be undone!"),
|
||||||
|
Some(InlineKeyboardMarkup::default().append_row([
|
||||||
|
InlineKeyboardButton::callback(
|
||||||
|
format!("Confirm bid amount: {bid_amount}"),
|
||||||
|
"confirm_bid",
|
||||||
|
),
|
||||||
|
InlineKeyboardButton::callback("Cancel", "cancel_bid"),
|
||||||
|
])),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
dialogue
|
||||||
|
.update(BiddingState::AwaitingConfirmBidAmount(
|
||||||
|
listing.persisted.id,
|
||||||
|
bid_amount,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.context("failed to update dialogue")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_awaiting_confirm_bid_amount_callback(
|
||||||
|
bot: Bot,
|
||||||
|
listing: PersistedListing,
|
||||||
|
(_, bid_amount): (ListingDbId, MoneyAmount),
|
||||||
|
target: MessageTarget,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
callback_query: CallbackQuery,
|
||||||
|
) -> BotResult {
|
||||||
|
let callback_data = callback_query
|
||||||
|
.data
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(BotError::user_visible("Missing data in callback query"))?;
|
||||||
|
|
||||||
|
let bid_amount = match callback_data {
|
||||||
|
"confirm_bid" => bid_amount,
|
||||||
|
"cancel_bid" => {
|
||||||
|
dialogue.exit().await.context("failed to exit dialogue")?;
|
||||||
|
send_message(&bot, target, "Bid cancelled", None).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(BotError::user_visible(format!(
|
||||||
|
"Invalid response {callback_data}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dialogue.exit().await.context("failed to exit dialogue")?;
|
||||||
|
|
||||||
|
send_message(
|
||||||
|
&bot,
|
||||||
|
target.only_chat_id(),
|
||||||
|
format!(
|
||||||
|
"Bid placed for {}{} on {}",
|
||||||
|
listing.base.currency_type.symbol(),
|
||||||
|
bid_amount,
|
||||||
|
listing.base.title
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO - keyboard with buttons to:
|
||||||
|
// - be notified if they are outbid
|
||||||
|
// - be notified when the auction ends
|
||||||
|
// - view details about the auction
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -7,6 +7,11 @@ pub enum BotError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
InternalError(#[from] anyhow::Error),
|
InternalError(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
impl BotError {
|
||||||
|
pub fn user_visible(msg: impl Into<String>) -> Self {
|
||||||
|
Self::UserVisibleError(msg.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type BotResult<T = ()> = Result<T, BotError>;
|
pub type BotResult<T = ()> = Result<T, BotError>;
|
||||||
pub type BotHandler = dptree::Handler<'static, BotResult, DpHandlerDescription>;
|
pub type BotHandler = dptree::Handler<'static, BotResult, DpHandlerDescription>;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::{
|
|||||||
my_listings::keyboard::{ManageListingButtons, MyListingsButtons},
|
my_listings::keyboard::{ManageListingButtons, MyListingsButtons},
|
||||||
new_listing::{
|
new_listing::{
|
||||||
enter_edit_listing_draft, enter_select_new_listing_type, keyboard::NavKeyboardButtons,
|
enter_edit_listing_draft, enter_select_new_listing_type, keyboard::NavKeyboardButtons,
|
||||||
ListingDraft,
|
messages::steps_for_listing_type, ListingDraft,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
db::{
|
db::{
|
||||||
@@ -15,17 +15,16 @@ use crate::{
|
|||||||
user::PersistedUser,
|
user::PersistedUser,
|
||||||
ListingDAO, ListingDbId, ListingType,
|
ListingDAO, ListingDbId, ListingType,
|
||||||
},
|
},
|
||||||
handler_utils::{
|
handle_error::with_error_handler,
|
||||||
callback_query_into_message_target, find_or_create_db_user_from_callback_query,
|
handler_utils::{find_listing_by_id, find_or_create_db_user_from_update},
|
||||||
find_or_create_db_user_from_message, message_into_message_target,
|
|
||||||
},
|
|
||||||
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
|
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
|
||||||
|
start_command_data::StartCommandData,
|
||||||
BotError, BotResult, Command, DialogueRootState, RootDialogue,
|
BotError, BotResult, Command, DialogueRootState, RootDialogue,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
|
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
||||||
use log::info;
|
use log::info;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::SqlitePool;
|
|
||||||
use teloxide::{
|
use teloxide::{
|
||||||
dispatching::{DpHandlerDescription, UpdateFilterExt},
|
dispatching::{DpHandlerDescription, UpdateFilterExt},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
@@ -57,46 +56,54 @@ pub fn my_listings_handler() -> Handler<'static, BotResult, DpHandlerDescription
|
|||||||
dptree::entry()
|
dptree::entry()
|
||||||
.branch(
|
.branch(
|
||||||
Update::filter_message()
|
Update::filter_message()
|
||||||
.filter_command::<Command>()
|
.filter_map(StartCommandData::get_from_update)
|
||||||
.map(message_into_message_target)
|
.filter_map(StartCommandData::get_view_listing_details_start_command)
|
||||||
.branch(
|
.filter_map_async(find_listing_by_id)
|
||||||
dptree::case![Command::MyListings]
|
.endpoint(with_error_handler(handle_view_listing_details)),
|
||||||
.filter_map_async(find_or_create_db_user_from_message)
|
)
|
||||||
.endpoint(handle_my_listings_command_input),
|
.branch(
|
||||||
),
|
Update::filter_message().filter_command::<Command>().branch(
|
||||||
|
dptree::case![Command::MyListings]
|
||||||
|
.filter_map_async(find_or_create_db_user_from_update)
|
||||||
|
.endpoint(with_error_handler(handle_my_listings_command_input)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
Update::filter_callback_query()
|
Update::filter_callback_query()
|
||||||
.filter_map(callback_query_into_message_target)
|
|
||||||
.branch(
|
.branch(
|
||||||
// Callback when user taps a listing ID button to manage that listing
|
// Callback when user taps a listing ID button to manage that listing
|
||||||
case![DialogueRootState::MyListings(
|
case![DialogueRootState::MyListings(
|
||||||
MyListingsState::ViewingListings
|
MyListingsState::ViewingListings
|
||||||
)]
|
)]
|
||||||
.filter_map_async(find_or_create_db_user_from_callback_query)
|
.endpoint(with_error_handler(handle_viewing_listings_callback)),
|
||||||
.endpoint(handle_viewing_listings_callback),
|
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::MyListings(
|
case![DialogueRootState::MyListings(
|
||||||
MyListingsState::ManagingListing(listing_id)
|
MyListingsState::ManagingListing(listing_id)
|
||||||
)]
|
)]
|
||||||
.filter_map_async(find_or_create_db_user_from_callback_query)
|
.endpoint(with_error_handler(handle_managing_listing_callback)),
|
||||||
.endpoint(handle_managing_listing_callback),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_view_listing_details(
|
||||||
|
bot: Bot,
|
||||||
|
listing: PersistedListing,
|
||||||
|
target: MessageTarget,
|
||||||
|
) -> BotResult {
|
||||||
|
send_listing_details_message(&bot, target, listing, None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn inline_query_extract_forward_listing(
|
async fn inline_query_extract_forward_listing(
|
||||||
db_pool: SqlitePool,
|
listing_dao: ListingDAO,
|
||||||
inline_query: InlineQuery,
|
inline_query: InlineQuery,
|
||||||
) -> Option<PersistedListing> {
|
) -> Option<PersistedListing> {
|
||||||
let query = &inline_query.query;
|
let query = &inline_query.query;
|
||||||
info!("Try to extract forward listing from query: {query}");
|
info!("Try to extract forward listing from query: {query}");
|
||||||
let listing_id_str = query.split("forward_listing:").nth(1)?;
|
let listing_id_str = query.split("forward_listing:").nth(1)?;
|
||||||
let listing_id = ListingDbId::new(listing_id_str.parse::<i64>().ok()?);
|
let listing_id = ListingDbId::new(listing_id_str.parse::<i64>().ok()?);
|
||||||
let listing = ListingDAO::find_by_id(&db_pool, listing_id)
|
let listing = listing_dao.find_by_id(listing_id).await.unwrap_or(None)?;
|
||||||
.await
|
|
||||||
.unwrap_or(None)?;
|
|
||||||
Some(listing)
|
Some(listing)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +128,16 @@ async fn handle_forward_listing(
|
|||||||
// Create inline keyboard with auction interaction buttons
|
// Create inline keyboard with auction interaction buttons
|
||||||
let keyboard = InlineKeyboardMarkup::default()
|
let keyboard = InlineKeyboardMarkup::default()
|
||||||
.append_row([
|
.append_row([
|
||||||
InlineKeyboardButton::callback(
|
InlineKeyboardButton::url(
|
||||||
"💰 Place Bid",
|
"💰 Place Bid?",
|
||||||
format!("inline_bid:{}", listing.persisted.id),
|
format!(
|
||||||
|
"tg://resolve?domain={}&start={}",
|
||||||
|
bot_username,
|
||||||
|
BASE64_URL_SAFE
|
||||||
|
.encode(format!("place_bid_on_listing:{}", listing.persisted.id))
|
||||||
|
)
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
),
|
),
|
||||||
InlineKeyboardButton::callback(
|
InlineKeyboardButton::callback(
|
||||||
"👀 Watch",
|
"👀 Watch",
|
||||||
@@ -133,8 +147,9 @@ async fn handle_forward_listing(
|
|||||||
.append_row([InlineKeyboardButton::url(
|
.append_row([InlineKeyboardButton::url(
|
||||||
"🔗 View Full Details",
|
"🔗 View Full Details",
|
||||||
format!(
|
format!(
|
||||||
"https://t.me/{}?start=listing:{}",
|
"tg://resolve?domain={}&start={}",
|
||||||
bot_username, listing.persisted.id
|
bot_username,
|
||||||
|
BASE64_URL_SAFE.encode(format!("view_listing_details:{}", listing.persisted.id))
|
||||||
)
|
)
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
@@ -204,18 +219,18 @@ fn get_listing_current_price(listing: &PersistedListing) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_my_listings_command_input(
|
async fn handle_my_listings_command_input(
|
||||||
db_pool: SqlitePool,
|
listing_dao: ListingDAO,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
enter_my_listings(db_pool, bot, dialogue, user, target, None).await?;
|
enter_my_listings(listing_dao, bot, dialogue, user, target, None).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn enter_my_listings(
|
pub async fn enter_my_listings(
|
||||||
db_pool: SqlitePool,
|
listing_dao: ListingDAO,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
@@ -228,7 +243,7 @@ pub async fn enter_my_listings(
|
|||||||
.await
|
.await
|
||||||
.context("failed to update dialogue")?;
|
.context("failed to update dialogue")?;
|
||||||
|
|
||||||
let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?;
|
let listings = listing_dao.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 {
|
||||||
@@ -267,7 +282,7 @@ pub async fn enter_my_listings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_viewing_listings_callback(
|
async fn handle_viewing_listings_callback(
|
||||||
db_pool: SqlitePool,
|
listing_dao: ListingDAO,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
@@ -284,7 +299,7 @@ async fn handle_viewing_listings_callback(
|
|||||||
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(&db_pool, user, listing_id).await?;
|
let listing = get_listing_for_user(&listing_dao, user, listing_id).await?;
|
||||||
|
|
||||||
enter_show_listing_details(&bot, dialogue, listing, target).await?;
|
enter_show_listing_details(&bot, dialogue, listing, target).await?;
|
||||||
}
|
}
|
||||||
@@ -302,19 +317,7 @@ async fn enter_show_listing_details(
|
|||||||
listing: PersistedListing,
|
listing: PersistedListing,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
let listing_type = Into::<ListingType>::into(&listing.fields);
|
|
||||||
let listing_id = listing.persisted.id;
|
let listing_id = listing.persisted.id;
|
||||||
let response = format!(
|
|
||||||
"🔍 <b>{listing_type} Details</b>\n\n\
|
|
||||||
<b>Title:</b> {}\n\
|
|
||||||
<b>Description:</b> {}\n",
|
|
||||||
listing.base.title,
|
|
||||||
listing
|
|
||||||
.base
|
|
||||||
.description
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("No description"),
|
|
||||||
);
|
|
||||||
dialogue
|
dialogue
|
||||||
.update(MyListingsState::ManagingListing(listing_id))
|
.update(MyListingsState::ManagingListing(listing_id))
|
||||||
.await
|
.await
|
||||||
@@ -332,12 +335,34 @@ 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_message(bot, target, response, Some(keyboard)).await?;
|
send_listing_details_message(bot, target, listing, Some(keyboard)).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_listing_details_message(
|
||||||
|
bot: &Bot,
|
||||||
|
target: MessageTarget,
|
||||||
|
listing: PersistedListing,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult {
|
||||||
|
let listing_type = Into::<ListingType>::into(&listing.fields);
|
||||||
|
let mut response_lines = vec![format!("🔍 <b>{listing_type} Details</b>")];
|
||||||
|
response_lines.push("".to_string());
|
||||||
|
|
||||||
|
let draft = ListingDraft::from_persisted(listing);
|
||||||
|
for step in steps_for_listing_type(listing_type) {
|
||||||
|
let field_value = match (step.get_field_value)(&draft) {
|
||||||
|
Ok(value) => value.unwrap_or_else(|| "(none)".to_string()),
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
response_lines.push(format!("<b>{}:</b> {}", step.field_name, field_value));
|
||||||
|
}
|
||||||
|
send_message(bot, target, response_lines.join("\n"), keyboard).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_managing_listing_callback(
|
async fn handle_managing_listing_callback(
|
||||||
db_pool: SqlitePool,
|
listing_dao: ListingDAO,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
@@ -350,7 +375,8 @@ async fn handle_managing_listing_callback(
|
|||||||
|
|
||||||
match ManageListingButtons::try_from(data.as_str())? {
|
match ManageListingButtons::try_from(data.as_str())? {
|
||||||
ManageListingButtons::PreviewMessage => {
|
ManageListingButtons::PreviewMessage => {
|
||||||
let listing = ListingDAO::find_by_id(&db_pool, listing_id)
|
let listing = listing_dao
|
||||||
|
.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(&bot, listing, from).await?;
|
||||||
@@ -359,14 +385,14 @@ async fn handle_managing_listing_callback(
|
|||||||
unimplemented!("Forward listing not implemented");
|
unimplemented!("Forward listing not implemented");
|
||||||
}
|
}
|
||||||
ManageListingButtons::Edit => {
|
ManageListingButtons::Edit => {
|
||||||
let listing = get_listing_for_user(&db_pool, user, listing_id).await?;
|
let listing = get_listing_for_user(&listing_dao, 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(&bot, target, draft, dialogue, None).await?;
|
||||||
}
|
}
|
||||||
ManageListingButtons::Delete => {
|
ManageListingButtons::Delete => {
|
||||||
ListingDAO::delete_listing(&db_pool, listing_id).await?;
|
listing_dao.delete_listing(listing_id).await?;
|
||||||
enter_my_listings(
|
enter_my_listings(
|
||||||
db_pool,
|
listing_dao,
|
||||||
bot,
|
bot,
|
||||||
dialogue,
|
dialogue,
|
||||||
user,
|
user,
|
||||||
@@ -376,7 +402,7 @@ async fn handle_managing_listing_callback(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
ManageListingButtons::Back => {
|
ManageListingButtons::Back => {
|
||||||
enter_my_listings(db_pool, bot, dialogue, user, target, None).await?;
|
enter_my_listings(listing_dao, bot, dialogue, user, target, None).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,11 +461,11 @@ async fn send_preview_listing_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_listing_for_user(
|
async fn get_listing_for_user(
|
||||||
db_pool: &SqlitePool,
|
listing_dao: &ListingDAO,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
listing_id: ListingDbId,
|
listing_id: ListingDbId,
|
||||||
) -> BotResult<PersistedListing> {
|
) -> BotResult<PersistedListing> {
|
||||||
let listing = match ListingDAO::find_by_id(db_pool, listing_id).await? {
|
let listing = match listing_dao.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,17 +15,18 @@ use crate::{
|
|||||||
ui::enter_confirm_save_listing,
|
ui::enter_confirm_save_listing,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
db::{user::PersistedUser, CurrencyType, ListingDuration, ListingType, MoneyAmount},
|
db::{
|
||||||
|
user::PersistedUser, CurrencyType, ListingDAO, ListingDuration, ListingType, MoneyAmount,
|
||||||
|
},
|
||||||
message_utils::*,
|
message_utils::*,
|
||||||
BotResult, RootDialogue,
|
BotResult, RootDialogue,
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use sqlx::SqlitePool;
|
|
||||||
use teloxide::{types::CallbackQuery, Bot};
|
use teloxide::{types::CallbackQuery, Bot};
|
||||||
|
|
||||||
/// 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(
|
||||||
db_pool: SqlitePool,
|
listing_dao: ListingDAO,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
@@ -36,7 +37,7 @@ pub async fn handle_selecting_listing_type_callback(
|
|||||||
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(db_pool, bot, dialogue, user, target, None).await;
|
return enter_my_listings(listing_dao, bot, dialogue, user, target, None).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the listing type from callback data
|
// Parse the listing type from callback data
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
use super::{callbacks::*, handlers::*, types::*};
|
use super::{callbacks::*, handlers::*, types::*};
|
||||||
use crate::{
|
use crate::{case, handle_error::with_error_handler, BotHandler, Command, DialogueRootState};
|
||||||
case,
|
|
||||||
handler_utils::{
|
|
||||||
callback_query_into_message_target, find_or_create_db_user_from_callback_query,
|
|
||||||
message_into_message_target,
|
|
||||||
},
|
|
||||||
BotHandler, Command, DialogueRootState,
|
|
||||||
};
|
|
||||||
use teloxide::{dptree, prelude::*, types::Update};
|
use teloxide::{dptree, prelude::*, types::Update};
|
||||||
|
|
||||||
// Create the dialogue handler tree for new listing wizard
|
// Create the dialogue handler tree for new listing wizard
|
||||||
@@ -14,59 +7,56 @@ pub fn new_listing_handler() -> BotHandler {
|
|||||||
dptree::entry()
|
dptree::entry()
|
||||||
.branch(
|
.branch(
|
||||||
Update::filter_message()
|
Update::filter_message()
|
||||||
.map(message_into_message_target)
|
|
||||||
.branch(
|
.branch(
|
||||||
dptree::entry()
|
dptree::entry()
|
||||||
.filter_command::<Command>()
|
.filter_command::<Command>()
|
||||||
.chain(case![Command::NewListing])
|
.chain(case![Command::NewListing])
|
||||||
.endpoint(handle_new_listing_command),
|
.endpoint(with_error_handler(handle_new_listing_command)),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![DialogueRootState::NewListing(
|
||||||
NewListingState::AwaitingDraftField { field, draft }
|
NewListingState::AwaitingDraftField { field, draft }
|
||||||
)]
|
)]
|
||||||
.endpoint(handle_awaiting_draft_field_input),
|
.endpoint(with_error_handler(handle_awaiting_draft_field_input)),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![DialogueRootState::NewListing(
|
||||||
NewListingState::EditingDraftField { field, draft }
|
NewListingState::EditingDraftField { field, draft }
|
||||||
)]
|
)]
|
||||||
.endpoint(handle_editing_field_input),
|
.endpoint(with_error_handler(handle_editing_field_input)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
Update::filter_callback_query()
|
Update::filter_callback_query()
|
||||||
.filter_map(callback_query_into_message_target)
|
|
||||||
.filter_map_async(find_or_create_db_user_from_callback_query)
|
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![DialogueRootState::NewListing(
|
||||||
NewListingState::SelectingListingType
|
NewListingState::SelectingListingType
|
||||||
)]
|
)]
|
||||||
.endpoint(handle_selecting_listing_type_callback),
|
.endpoint(with_error_handler(handle_selecting_listing_type_callback)),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![DialogueRootState::NewListing(
|
||||||
NewListingState::AwaitingDraftField { field, draft }
|
NewListingState::AwaitingDraftField { field, draft }
|
||||||
)]
|
)]
|
||||||
.endpoint(handle_awaiting_draft_field_callback),
|
.endpoint(with_error_handler(handle_awaiting_draft_field_callback)),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![DialogueRootState::NewListing(
|
||||||
NewListingState::ViewingDraft(draft)
|
NewListingState::ViewingDraft(draft)
|
||||||
)]
|
)]
|
||||||
.endpoint(handle_viewing_draft_callback),
|
.endpoint(with_error_handler(handle_viewing_draft_callback)),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![DialogueRootState::NewListing(
|
||||||
NewListingState::EditingDraft(draft)
|
NewListingState::EditingDraft(draft)
|
||||||
)]
|
)]
|
||||||
.endpoint(handle_editing_draft_callback),
|
.endpoint(with_error_handler(handle_editing_draft_callback)),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![DialogueRootState::NewListing(
|
||||||
NewListingState::EditingDraftField { field, draft }
|
NewListingState::EditingDraftField { field, draft }
|
||||||
)]
|
)]
|
||||||
.endpoint(handle_editing_draft_field_callback),
|
.endpoint(with_error_handler(handle_editing_draft_field_callback)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,10 @@ use crate::{
|
|||||||
ListingDAO,
|
ListingDAO,
|
||||||
},
|
},
|
||||||
message_utils::*,
|
message_utils::*,
|
||||||
BotResult, DialogueRootState, RootDialogue,
|
BotError, BotResult, DialogueRootState, RootDialogue,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use log::info;
|
use log::info;
|
||||||
use sqlx::SqlitePool;
|
|
||||||
use teloxide::{prelude::*, types::*, Bot};
|
use teloxide::{prelude::*, types::*, Bot};
|
||||||
|
|
||||||
/// Handle the /newlisting command - starts the dialogue
|
/// Handle the /newlisting command - starts the dialogue
|
||||||
@@ -80,8 +79,7 @@ pub async fn handle_awaiting_draft_field_input(
|
|||||||
match update_field_on_draft(field, &mut draft, msg.text()) {
|
match update_field_on_draft(field, &mut draft, msg.text()) {
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
Err(SetFieldError::ValidationFailed(e)) => {
|
Err(SetFieldError::ValidationFailed(e)) => {
|
||||||
send_message(&bot, target, e.clone(), None).await?;
|
return Err(BotError::user_visible(e));
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
Err(SetFieldError::UnsupportedFieldForListingType) => {
|
Err(SetFieldError::UnsupportedFieldForListingType) => {
|
||||||
return Err(anyhow!("Cannot update field {field:?} for listing type").into());
|
return Err(anyhow!("Cannot update field {field:?} for listing type").into());
|
||||||
@@ -121,8 +119,7 @@ pub async fn handle_editing_field_input(
|
|||||||
match update_field_on_draft(field, &mut draft, msg.text()) {
|
match update_field_on_draft(field, &mut draft, msg.text()) {
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
Err(SetFieldError::ValidationFailed(e)) => {
|
Err(SetFieldError::ValidationFailed(e)) => {
|
||||||
send_message(&bot, target, e.clone(), None).await?;
|
return Err(BotError::user_visible(e));
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
Err(SetFieldError::UnsupportedFieldForListingType) => {
|
Err(SetFieldError::UnsupportedFieldForListingType) => {
|
||||||
return Err(anyhow!("Cannot update field {field:?} for listing type").into());
|
return Err(anyhow!("Cannot update field {field:?} for listing type").into());
|
||||||
@@ -139,7 +136,7 @@ pub async fn handle_editing_field_input(
|
|||||||
|
|
||||||
/// Handle viewing draft confirmation callbacks
|
/// Handle viewing draft confirmation callbacks
|
||||||
pub async fn handle_viewing_draft_callback(
|
pub async fn handle_viewing_draft_callback(
|
||||||
db_pool: SqlitePool,
|
listing_dao: ListingDAO,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
draft: ListingDraft,
|
draft: ListingDraft,
|
||||||
@@ -152,8 +149,16 @@ pub async fn handle_viewing_draft_callback(
|
|||||||
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(&db_pool, draft).await?;
|
let success_message = save_listing(&listing_dao, draft).await?;
|
||||||
enter_my_listings(db_pool, bot, dialogue, user, target, Some(success_message)).await?;
|
enter_my_listings(
|
||||||
|
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");
|
||||||
@@ -272,28 +277,24 @@ pub async fn enter_edit_listing_draft(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Save the listing to the database
|
/// Save the listing to the database
|
||||||
async fn save_listing(db_pool: &SqlitePool, draft: ListingDraft) -> BotResult<String> {
|
async fn save_listing(listing_dao: &ListingDAO, draft: ListingDraft) -> BotResult<String> {
|
||||||
let (listing, success_message) = if let Some(fields) = draft.persisted {
|
let (listing, success_message) = if let Some(fields) = draft.persisted {
|
||||||
let listing = ListingDAO::update_listing(
|
let listing = listing_dao
|
||||||
db_pool,
|
.update_listing(PersistedListing {
|
||||||
PersistedListing {
|
|
||||||
persisted: fields,
|
persisted: fields,
|
||||||
base: draft.base,
|
base: draft.base,
|
||||||
fields: draft.fields,
|
fields: draft.fields,
|
||||||
},
|
})
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
(listing, "Listing updated!")
|
(listing, "Listing updated!")
|
||||||
} else {
|
} else {
|
||||||
let listing = ListingDAO::insert_listing(
|
let listing = listing_dao
|
||||||
db_pool,
|
.insert_listing(NewListing {
|
||||||
NewListing {
|
|
||||||
persisted: (),
|
persisted: (),
|
||||||
base: draft.base,
|
base: draft.base,
|
||||||
fields: draft.fields,
|
fields: draft.fields,
|
||||||
},
|
})
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
(listing, "Listing created!")
|
(listing, "Listing created!")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub mod messages;
|
|||||||
mod tests;
|
mod tests;
|
||||||
mod types;
|
mod types;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod validations;
|
pub mod validations;
|
||||||
|
|
||||||
// Re-export the main handler for external use
|
// Re-export the main handler for external use
|
||||||
pub use handler_factory::new_listing_handler;
|
pub use handler_factory::new_listing_handler;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use log::info;
|
use log::info;
|
||||||
use teloxide::{types::CallbackQuery, utils::command::BotCommands, Bot};
|
use teloxide::{
|
||||||
|
types::{CallbackQuery, Update},
|
||||||
use sqlx::SqlitePool;
|
utils::command::BotCommands,
|
||||||
|
Bot,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::my_listings::enter_my_listings,
|
commands::my_listings::enter_my_listings,
|
||||||
db::user::PersistedUser,
|
db::{user::PersistedUser, ListingDAO},
|
||||||
keyboard_buttons,
|
keyboard_buttons,
|
||||||
message_utils::{extract_callback_data, send_message, MessageTarget},
|
message_utils::{extract_callback_data, send_message, MessageTarget},
|
||||||
BotResult, Command, DialogueRootState, RootDialogue,
|
BotResult, Command, DialogueRootState, RootDialogue,
|
||||||
@@ -26,7 +28,7 @@ keyboard_buttons! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the main menu welcome message
|
/// Get the main menu welcome message
|
||||||
pub fn get_main_menu_message() -> &'static str {
|
fn get_main_menu_message() -> &'static str {
|
||||||
"🎯 <b>Welcome to Pawctioneer Bot!</b> 🎯\n\n\
|
"🎯 <b>Welcome to Pawctioneer Bot!</b> 🎯\n\n\
|
||||||
This bot helps you participate in various types of auctions:\n\
|
This bot helps you participate in various types of auctions:\n\
|
||||||
• Standard auctions with anti-sniping protection\n\
|
• Standard auctions with anti-sniping protection\n\
|
||||||
@@ -36,7 +38,13 @@ pub fn get_main_menu_message() -> &'static str {
|
|||||||
Choose an option below to get started! 🚀"
|
Choose an option below to get started! 🚀"
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_start(bot: Bot, dialogue: RootDialogue, target: MessageTarget) -> BotResult {
|
pub async fn handle_start(
|
||||||
|
bot: Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
target: MessageTarget,
|
||||||
|
update: Update,
|
||||||
|
) -> BotResult {
|
||||||
|
info!("got start message: {update:?}");
|
||||||
enter_main_menu(bot, dialogue, target).await?;
|
enter_main_menu(bot, dialogue, target).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -60,7 +68,7 @@ pub async fn enter_main_menu(bot: Bot, dialogue: RootDialogue, target: MessageTa
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_main_menu_callback(
|
pub async fn handle_main_menu_callback(
|
||||||
db_pool: SqlitePool,
|
listing_dao: ListingDAO,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
@@ -74,7 +82,7 @@ pub async fn handle_main_menu_callback(
|
|||||||
match button {
|
match button {
|
||||||
MainMenuButtons::MyListings => {
|
MainMenuButtons::MyListings => {
|
||||||
// Call show_listings_for_user directly
|
// Call show_listings_for_user directly
|
||||||
enter_my_listings(db_pool, bot, dialogue, user, target, None).await?;
|
enter_my_listings(listing_dao, bot, dialogue, user, target, None).await?;
|
||||||
}
|
}
|
||||||
MainMenuButtons::MyBids => {
|
MainMenuButtons::MyBids => {
|
||||||
send_message(
|
send_message(
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ use crate::db::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Data Access Object for Listing operations
|
/// Data Access Object for Listing operations
|
||||||
pub struct ListingDAO;
|
#[derive(Clone)]
|
||||||
|
pub struct ListingDAO(SqlitePool);
|
||||||
|
|
||||||
const LISTING_RETURN_FIELDS: &[&str] = &[
|
const LISTING_RETURN_FIELDS: &[&str] = &[
|
||||||
"id",
|
"id",
|
||||||
@@ -40,11 +41,12 @@ const LISTING_RETURN_FIELDS: &[&str] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
impl ListingDAO {
|
impl ListingDAO {
|
||||||
|
pub fn new(pool: SqlitePool) -> Self {
|
||||||
|
Self(pool)
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert a new listing into the database
|
/// Insert a new listing into the database
|
||||||
pub async fn insert_listing(
|
pub async fn insert_listing(&self, listing: NewListing) -> Result<PersistedListing> {
|
||||||
pool: &SqlitePool,
|
|
||||||
listing: NewListing,
|
|
||||||
) -> Result<PersistedListing> {
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
let binds = binds_for_listing(&listing)
|
let binds = binds_for_listing(&listing)
|
||||||
@@ -66,15 +68,12 @@ impl ListingDAO {
|
|||||||
|
|
||||||
let row = binds
|
let row = binds
|
||||||
.bind_to_query(sqlx::query(&query_str))
|
.bind_to_query(sqlx::query(&query_str))
|
||||||
.fetch_one(pool)
|
.fetch_one(&self.0)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(FromRow::from_row(&row)?)
|
Ok(FromRow::from_row(&row)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_listing(
|
pub async fn update_listing(&self, listing: PersistedListing) -> Result<PersistedListing> {
|
||||||
pool: &SqlitePool,
|
|
||||||
listing: PersistedListing,
|
|
||||||
) -> Result<PersistedListing> {
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let binds = binds_for_listing(&listing).push("updated_at", &now);
|
let binds = binds_for_listing(&listing).push("updated_at", &now);
|
||||||
|
|
||||||
@@ -97,47 +96,41 @@ impl ListingDAO {
|
|||||||
.bind_to_query(sqlx::query(&query_str))
|
.bind_to_query(sqlx::query(&query_str))
|
||||||
.bind(listing.persisted.id)
|
.bind(listing.persisted.id)
|
||||||
.bind(listing.base.seller_id)
|
.bind(listing.base.seller_id)
|
||||||
.fetch_one(pool)
|
.fetch_one(&self.0)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(FromRow::from_row(&row)?)
|
Ok(FromRow::from_row(&row)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a listing by its ID
|
/// Find a listing by its ID
|
||||||
pub async fn find_by_id(
|
pub async fn find_by_id(&self, listing_id: ListingDbId) -> Result<Option<PersistedListing>> {
|
||||||
pool: &SqlitePool,
|
|
||||||
listing_id: ListingDbId,
|
|
||||||
) -> Result<Option<PersistedListing>> {
|
|
||||||
let result = sqlx::query_as(&format!(
|
let result = sqlx::query_as(&format!(
|
||||||
"SELECT {} FROM listings WHERE id = ?",
|
"SELECT {} FROM listings WHERE id = ?",
|
||||||
LISTING_RETURN_FIELDS.join(", ")
|
LISTING_RETURN_FIELDS.join(", ")
|
||||||
))
|
))
|
||||||
.bind(listing_id)
|
.bind(listing_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(&self.0)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find all listings by a seller
|
/// Find all listings by a seller
|
||||||
pub async fn find_by_seller(
|
pub async fn find_by_seller(&self, seller_id: UserDbId) -> Result<Vec<PersistedListing>> {
|
||||||
pool: &SqlitePool,
|
|
||||||
seller_id: UserDbId,
|
|
||||||
) -> Result<Vec<PersistedListing>> {
|
|
||||||
let rows = sqlx::query_as(&format!(
|
let rows = sqlx::query_as(&format!(
|
||||||
"SELECT {} FROM listings WHERE seller_id = ? ORDER BY created_at DESC",
|
"SELECT {} FROM listings WHERE seller_id = ? ORDER BY created_at DESC",
|
||||||
LISTING_RETURN_FIELDS.join(", ")
|
LISTING_RETURN_FIELDS.join(", ")
|
||||||
))
|
))
|
||||||
.bind(seller_id)
|
.bind(seller_id)
|
||||||
.fetch_all(pool)
|
.fetch_all(&self.0)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(rows)
|
Ok(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a listing
|
/// Delete a listing
|
||||||
pub async fn delete_listing(pool: &SqlitePool, listing_id: ListingDbId) -> Result<()> {
|
pub async fn delete_listing(&self, listing_id: ListingDbId) -> Result<()> {
|
||||||
sqlx::query("DELETE FROM listings WHERE id = ?")
|
sqlx::query("DELETE FROM listings WHERE id = ?")
|
||||||
.bind(listing_id)
|
.bind(listing_id)
|
||||||
.execute(pool)
|
.execute(&self.0)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ use crate::db::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Data Access Object for User operations
|
/// Data Access Object for User operations
|
||||||
pub struct UserDAO;
|
#[derive(Clone)]
|
||||||
|
pub struct UserDAO(SqlitePool);
|
||||||
|
|
||||||
const USER_RETURN_FIELDS: &[&str] = &[
|
const USER_RETURN_FIELDS: &[&str] = &[
|
||||||
"id",
|
"id",
|
||||||
@@ -29,8 +30,12 @@ const USER_RETURN_FIELDS: &[&str] = &[
|
|||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
impl UserDAO {
|
impl UserDAO {
|
||||||
|
pub fn new(pool: SqlitePool) -> Self {
|
||||||
|
Self(pool)
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert a new user into the database
|
/// Insert a new user into the database
|
||||||
pub async fn insert_user(pool: &SqlitePool, new_user: &NewUser) -> Result<PersistedUser> {
|
pub async fn insert_user(&self, new_user: &NewUser) -> Result<PersistedUser> {
|
||||||
let binds = BindFields::default()
|
let binds = BindFields::default()
|
||||||
.push("telegram_id", &new_user.telegram_id)
|
.push("telegram_id", &new_user.telegram_id)
|
||||||
.push("first_name", &new_user.first_name)
|
.push("first_name", &new_user.first_name)
|
||||||
@@ -48,13 +53,13 @@ impl UserDAO {
|
|||||||
USER_RETURN_FIELDS.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(pool).await?;
|
let row = binds.bind_to_query(query).fetch_one(&self.0).await?;
|
||||||
|
|
||||||
Ok(FromRow::from_row(&row)?)
|
Ok(FromRow::from_row(&row)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a user by their ID
|
/// Find a user by their ID
|
||||||
pub async fn find_by_id(pool: &SqlitePool, 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(sqlx::query_as::<_, PersistedUser>(
|
||||||
r#"
|
r#"
|
||||||
SELECT id, telegram_id, first_name, last_name, username, is_banned, created_at, updated_at
|
SELECT id, telegram_id, first_name, last_name, username, is_banned, created_at, updated_at
|
||||||
@@ -63,13 +68,13 @@ impl UserDAO {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(&self.0)
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a user by their Telegram ID
|
/// Find a user by their Telegram ID
|
||||||
pub async fn find_by_telegram_id(
|
pub async fn find_by_telegram_id(
|
||||||
pool: &SqlitePool,
|
&self,
|
||||||
telegram_id: impl Into<TelegramUserDbId>,
|
telegram_id: impl Into<TelegramUserDbId>,
|
||||||
) -> Result<Option<PersistedUser>> {
|
) -> Result<Option<PersistedUser>> {
|
||||||
let telegram_id = telegram_id.into();
|
let telegram_id = telegram_id.into();
|
||||||
@@ -81,12 +86,12 @@ impl UserDAO {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(telegram_id)
|
.bind(telegram_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(&self.0)
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_or_create_by_telegram_user(
|
pub async fn find_or_create_by_telegram_user(
|
||||||
pool: &SqlitePool,
|
&self,
|
||||||
user: teloxide::types::User,
|
user: teloxide::types::User,
|
||||||
) -> Result<PersistedUser> {
|
) -> Result<PersistedUser> {
|
||||||
let binds = BindFields::default()
|
let binds = BindFields::default()
|
||||||
@@ -112,7 +117,7 @@ impl UserDAO {
|
|||||||
|
|
||||||
let row = binds
|
let row = binds
|
||||||
.bind_to_query(sqlx::query(&query_str))
|
.bind_to_query(sqlx::query(&query_str))
|
||||||
.fetch_one(pool)
|
.fetch_one(&self.0)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let user = FromRow::from_row(&row)?;
|
let user = FromRow::from_row(&row)?;
|
||||||
@@ -121,7 +126,7 @@ impl UserDAO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update a user's information
|
/// Update a user's information
|
||||||
pub async fn update_user(pool: &SqlitePool, user: &PersistedUser) -> Result<PersistedUser> {
|
pub async fn update_user(&self, user: &PersistedUser) -> Result<PersistedUser> {
|
||||||
let updated_user = sqlx::query_as::<_, PersistedUser>(
|
let updated_user = sqlx::query_as::<_, PersistedUser>(
|
||||||
r#"
|
r#"
|
||||||
UPDATE users
|
UPDATE users
|
||||||
@@ -135,32 +140,28 @@ impl UserDAO {
|
|||||||
.bind(&user.last_name)
|
.bind(&user.last_name)
|
||||||
.bind(user.is_banned) // sqlx automatically converts bool to INTEGER for SQLite
|
.bind(user.is_banned) // sqlx automatically converts bool to INTEGER for SQLite
|
||||||
.bind(user.persisted.id)
|
.bind(user.persisted.id)
|
||||||
.fetch_one(pool)
|
.fetch_one(&self.0)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(updated_user)
|
Ok(updated_user)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a user's ban status
|
/// Set a user's ban status
|
||||||
pub async fn set_ban_status(
|
pub async fn set_ban_status(&self, user_id: UserDbId, is_banned: bool) -> Result<()> {
|
||||||
pool: &SqlitePool,
|
|
||||||
user_id: UserDbId,
|
|
||||||
is_banned: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
sqlx::query("UPDATE users SET is_banned = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
|
sqlx::query("UPDATE users SET is_banned = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
|
||||||
.bind(is_banned) // sqlx automatically converts bool to INTEGER for SQLite
|
.bind(is_banned) // sqlx automatically converts bool to INTEGER for SQLite
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.execute(pool)
|
.execute(&self.0)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a user (soft delete by setting is_banned = true might be better in production)
|
/// Delete a user (soft delete by setting is_banned = true might be better in production)
|
||||||
pub async fn delete_user(pool: &SqlitePool, user_id: UserDbId) -> Result<()> {
|
pub async fn delete_user(&self, user_id: UserDbId) -> Result<()> {
|
||||||
sqlx::query("DELETE FROM users WHERE id = ?")
|
sqlx::query("DELETE FROM users WHERE id = ?")
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.execute(pool)
|
.execute(&self.0)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -194,7 +195,7 @@ mod tests {
|
|||||||
use teloxide::types::UserId;
|
use teloxide::types::UserId;
|
||||||
|
|
||||||
/// Create test database for UserDAO tests
|
/// Create test database for UserDAO tests
|
||||||
async fn create_test_pool() -> SqlitePool {
|
async fn create_test_dao() -> UserDAO {
|
||||||
let pool = SqlitePool::connect("sqlite::memory:")
|
let pool = SqlitePool::connect("sqlite::memory:")
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create in-memory database");
|
.expect("Failed to create in-memory database");
|
||||||
@@ -205,12 +206,12 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to run database migrations");
|
.expect("Failed to run database migrations");
|
||||||
|
|
||||||
pool
|
UserDAO::new(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_insert_and_find_user() {
|
async fn test_insert_and_find_user() {
|
||||||
let pool = create_test_pool().await;
|
let dao = create_test_dao().await;
|
||||||
|
|
||||||
let new_user = NewUser {
|
let new_user = NewUser {
|
||||||
persisted: (),
|
persisted: (),
|
||||||
@@ -222,7 +223,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Insert user
|
// Insert user
|
||||||
let inserted_user = UserDAO::insert_user(&pool, &new_user)
|
let inserted_user = dao
|
||||||
|
.insert_user(&new_user)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to insert user");
|
.expect("Failed to insert user");
|
||||||
|
|
||||||
@@ -232,7 +234,8 @@ mod tests {
|
|||||||
assert!(!inserted_user.is_banned);
|
assert!(!inserted_user.is_banned);
|
||||||
|
|
||||||
// Find by ID
|
// Find by ID
|
||||||
let found_user = UserDAO::find_by_id(&pool, inserted_user.persisted.id)
|
let found_user = dao
|
||||||
|
.find_by_id(inserted_user.persisted.id)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to find user by id")
|
.expect("Failed to find user by id")
|
||||||
.expect("User should be found");
|
.expect("User should be found");
|
||||||
@@ -241,7 +244,8 @@ mod tests {
|
|||||||
assert_eq!(found_user.telegram_id, inserted_user.telegram_id);
|
assert_eq!(found_user.telegram_id, inserted_user.telegram_id);
|
||||||
|
|
||||||
// Find by telegram ID
|
// Find by telegram ID
|
||||||
let found_by_telegram = UserDAO::find_by_telegram_id(&pool, UserId(12345))
|
let found_by_telegram = dao
|
||||||
|
.find_by_telegram_id(UserId(12345))
|
||||||
.await
|
.await
|
||||||
.expect("Failed to find user by telegram_id")
|
.expect("Failed to find user by telegram_id")
|
||||||
.expect("User should be found");
|
.expect("User should be found");
|
||||||
@@ -252,12 +256,11 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_get_or_create_user() {
|
async fn test_get_or_create_user() {
|
||||||
let pool = create_test_pool().await;
|
let dao = create_test_dao().await;
|
||||||
|
|
||||||
// First call should create the user
|
// First call should create the user
|
||||||
let user1 = UserDAO::find_or_create_by_telegram_user(
|
let user1 = dao
|
||||||
&pool,
|
.find_or_create_by_telegram_user(teloxide::types::User {
|
||||||
teloxide::types::User {
|
|
||||||
id: UserId(67890),
|
id: UserId(67890),
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
first_name: "New User".to_string(),
|
first_name: "New User".to_string(),
|
||||||
@@ -266,18 +269,16 @@ mod tests {
|
|||||||
language_code: None,
|
language_code: None,
|
||||||
is_premium: false,
|
is_premium: false,
|
||||||
added_to_attachment_menu: false,
|
added_to_attachment_menu: false,
|
||||||
},
|
})
|
||||||
)
|
.await
|
||||||
.await
|
.expect("Failed to get or create user");
|
||||||
.expect("Failed to get or create user");
|
|
||||||
|
|
||||||
assert_eq!(user1.telegram_id, 67890.into());
|
assert_eq!(user1.telegram_id, 67890.into());
|
||||||
assert_eq!(user1.username, Some("newuser".to_string()));
|
assert_eq!(user1.username, Some("newuser".to_string()));
|
||||||
|
|
||||||
// Second call should return the same user
|
// Second call should return the same user
|
||||||
let user2 = UserDAO::find_or_create_by_telegram_user(
|
let user2 = dao
|
||||||
&pool,
|
.find_or_create_by_telegram_user(teloxide::types::User {
|
||||||
teloxide::types::User {
|
|
||||||
id: UserId(67890),
|
id: UserId(67890),
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
first_name: "New User".to_string(),
|
first_name: "New User".to_string(),
|
||||||
@@ -286,10 +287,9 @@ mod tests {
|
|||||||
language_code: None,
|
language_code: None,
|
||||||
is_premium: false,
|
is_premium: false,
|
||||||
added_to_attachment_menu: false,
|
added_to_attachment_menu: false,
|
||||||
},
|
})
|
||||||
)
|
.await
|
||||||
.await
|
.expect("Failed to get or create user");
|
||||||
.expect("Failed to get or create user");
|
|
||||||
|
|
||||||
assert_eq!(user1.persisted.id, user2.persisted.id);
|
assert_eq!(user1.persisted.id, user2.persisted.id);
|
||||||
assert_eq!(user2.username, Some("newuser".to_string())); // Original username preserved
|
assert_eq!(user2.username, Some("newuser".to_string())); // Original username preserved
|
||||||
@@ -297,7 +297,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_update_user() {
|
async fn test_update_user() {
|
||||||
let pool = create_test_pool().await;
|
let pool = create_test_dao().await;
|
||||||
|
|
||||||
let new_user = NewUser {
|
let new_user = NewUser {
|
||||||
persisted: (),
|
persisted: (),
|
||||||
@@ -328,7 +328,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_user() {
|
async fn test_delete_user() {
|
||||||
let pool = create_test_pool().await;
|
let pool = create_test_dao().await;
|
||||||
|
|
||||||
let new_user = NewUser {
|
let new_user = NewUser {
|
||||||
persisted: (),
|
persisted: (),
|
||||||
@@ -358,7 +358,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_find_nonexistent_user() {
|
async fn test_find_nonexistent_user() {
|
||||||
let pool = create_test_pool().await;
|
let pool = create_test_dao().await;
|
||||||
|
|
||||||
// Try to find a user that doesn't exist
|
// Try to find a user that doesn't exist
|
||||||
let not_found = UserDAO::find_by_id(&pool, UserDbId::new(99999))
|
let not_found = UserDAO::find_by_id(&pool, UserDbId::new(99999))
|
||||||
@@ -390,7 +390,7 @@ mod tests {
|
|||||||
#[case] first_name: Option<&str>,
|
#[case] first_name: Option<&str>,
|
||||||
#[case] last_name: Option<&str>,
|
#[case] last_name: Option<&str>,
|
||||||
) {
|
) {
|
||||||
let pool = create_test_pool().await;
|
let pool = create_test_dao().await;
|
||||||
let user_id = UserId(12345);
|
let user_id = UserId(12345);
|
||||||
|
|
||||||
let initial = teloxide::types::User {
|
let initial = teloxide::types::User {
|
||||||
@@ -440,7 +440,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_multiple_users_separate() {
|
async fn test_multiple_users_separate() {
|
||||||
let pool = create_test_pool().await;
|
let pool = create_test_dao().await;
|
||||||
|
|
||||||
let user1 = teloxide::types::User {
|
let user1 = teloxide::types::User {
|
||||||
id: UserId(111),
|
id: UserId(111),
|
||||||
@@ -477,7 +477,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_upsert_preserves_id_and_timestamps() {
|
async fn test_upsert_preserves_id_and_timestamps() {
|
||||||
let pool = create_test_pool().await;
|
let pool = create_test_dao().await;
|
||||||
|
|
||||||
let user = teloxide::types::User {
|
let user = teloxide::types::User {
|
||||||
id: UserId(333),
|
id: UserId(333),
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ pub struct Listing<P: Debug + Clone> {
|
|||||||
pub type ListingBaseFields<'a> = (&'a ListingBase, &'a ListingFields);
|
pub type ListingBaseFields<'a> = (&'a ListingBase, &'a ListingFields);
|
||||||
pub type ListingBaseFieldsMut<'a> = (&'a mut ListingBase, &'a mut ListingFields);
|
pub type ListingBaseFieldsMut<'a> = (&'a mut ListingBase, &'a mut ListingFields);
|
||||||
|
|
||||||
impl<'a, P: Debug + Clone> Into<ListingBaseFields<'a>> for &'a Listing<P> {
|
impl<'a, P: Debug + Clone> From<&'a Listing<P>> for ListingBaseFields<'a> {
|
||||||
fn into(self) -> ListingBaseFields<'a> {
|
fn from(value: &'a Listing<P>) -> Self {
|
||||||
(&self.base, &self.fields)
|
(&value.base, &value.fields)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<'a, P: Debug + Clone> Into<ListingBaseFieldsMut<'a>> for &'a mut Listing<P> {
|
impl<'a, P: Debug + Clone> From<&'a mut Listing<P>> for ListingBaseFieldsMut<'a> {
|
||||||
fn into(self) -> ListingBaseFieldsMut<'a> {
|
fn from(value: &'a mut Listing<P>) -> Self {
|
||||||
(&mut self.base, &mut self.fields)
|
(&mut value.base, &mut value.fields)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,40 +135,17 @@ impl From<&ListingFields> for ListingType {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db::{ListingDAO, TelegramUserDbId};
|
use crate::db::{TelegramUserDbId, UserDAO};
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
use sqlx::SqlitePool;
|
|
||||||
|
|
||||||
/// Test utilities for creating an in-memory database with migrations
|
|
||||||
async fn create_test_pool() -> SqlitePool {
|
|
||||||
// Create an in-memory SQLite database for testing
|
|
||||||
let pool = SqlitePool::connect("sqlite::memory:")
|
|
||||||
.await
|
|
||||||
.expect("Failed to create in-memory database");
|
|
||||||
|
|
||||||
// Run the migration
|
|
||||||
apply_test_migrations(&pool).await;
|
|
||||||
|
|
||||||
pool
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply the database migrations for testing
|
|
||||||
async fn apply_test_migrations(pool: &SqlitePool) {
|
|
||||||
// Run the actual migrations from the migrations directory
|
|
||||||
sqlx::migrate!("./migrations")
|
|
||||||
.run(pool)
|
|
||||||
.await
|
|
||||||
.expect("Failed to run database migrations");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a test user using UserDAO and return their ID
|
/// Create a test user using UserDAO and return their ID
|
||||||
async fn create_test_user(
|
async fn create_test_user(
|
||||||
pool: &SqlitePool,
|
user_dao: &UserDAO,
|
||||||
telegram_id: TelegramUserDbId,
|
telegram_id: TelegramUserDbId,
|
||||||
username: Option<&str>,
|
username: Option<&str>,
|
||||||
) -> UserDbId {
|
) -> UserDbId {
|
||||||
use crate::db::{models::user::NewUser, UserDAO};
|
use crate::db::models::user::NewUser;
|
||||||
|
|
||||||
let new_user = NewUser {
|
let new_user = NewUser {
|
||||||
persisted: (),
|
persisted: (),
|
||||||
@@ -179,7 +156,8 @@ mod tests {
|
|||||||
is_banned: false,
|
is_banned: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user = UserDAO::insert_user(pool, &new_user)
|
let user = user_dao
|
||||||
|
.insert_user(&new_user)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create test user");
|
.expect("Failed to create test user");
|
||||||
user.persisted.id
|
user.persisted.id
|
||||||
@@ -208,8 +186,12 @@ mod tests {
|
|||||||
#[case(ListingFields::FixedPriceListing(FixedPriceListingFields { buy_now_price: MoneyAmount::from_str("100.00").unwrap(), slots_available: 10 }))]
|
#[case(ListingFields::FixedPriceListing(FixedPriceListingFields { buy_now_price: MoneyAmount::from_str("100.00").unwrap(), slots_available: 10 }))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_blind_auction_crud(#[case] fields: ListingFields) {
|
async fn test_blind_auction_crud(#[case] fields: ListingFields) {
|
||||||
|
use crate::{db::ListingDAO, test_utils::create_test_pool};
|
||||||
|
|
||||||
let pool = create_test_pool().await;
|
let pool = create_test_pool().await;
|
||||||
let seller_id = create_test_user(&pool, 99999.into(), Some("testuser")).await;
|
let user_dao = UserDAO::new(pool.clone());
|
||||||
|
let listing_dao = ListingDAO::new(pool.clone());
|
||||||
|
let seller_id = create_test_user(&user_dao, 99999.into(), Some("testuser")).await;
|
||||||
let new_listing = build_base_listing(
|
let new_listing = build_base_listing(
|
||||||
seller_id,
|
seller_id,
|
||||||
"Test Auction",
|
"Test Auction",
|
||||||
@@ -219,14 +201,16 @@ mod tests {
|
|||||||
.with_fields(fields);
|
.with_fields(fields);
|
||||||
|
|
||||||
// Insert using DAO
|
// Insert using DAO
|
||||||
let created_listing = ListingDAO::insert_listing(&pool, new_listing.clone())
|
let created_listing = listing_dao
|
||||||
|
.insert_listing(new_listing.clone())
|
||||||
.await
|
.await
|
||||||
.expect("Failed to insert listing");
|
.expect("Failed to insert listing");
|
||||||
|
|
||||||
assert_eq!(created_listing.base, new_listing.base);
|
assert_eq!(created_listing.base, new_listing.base);
|
||||||
assert_eq!(created_listing.fields, new_listing.fields);
|
assert_eq!(created_listing.fields, new_listing.fields);
|
||||||
|
|
||||||
let read_listing = ListingDAO::find_by_id(&pool, created_listing.persisted.id)
|
let read_listing = listing_dao
|
||||||
|
.find_by_id(created_listing.persisted.id)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to find listing")
|
.expect("Failed to find listing")
|
||||||
.expect("Listing should exist");
|
.expect("Listing should exist");
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ 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) => send_message(&bot, target, message, None).await?,
|
||||||
BotError::InternalError(error) => {
|
BotError::InternalError(_) => {
|
||||||
log::error!("Internal error: {error}");
|
|
||||||
send_message(
|
send_message(
|
||||||
&bot,
|
&bot,
|
||||||
target,
|
target,
|
||||||
|
|||||||
@@ -1,32 +1,24 @@
|
|||||||
use sqlx::SqlitePool;
|
use log::warn;
|
||||||
use teloxide::types::{CallbackQuery, Message};
|
use teloxide::types::Update;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{user::PersistedUser, UserDAO},
|
db::{listing::PersistedListing, user::PersistedUser, ListingDAO, ListingDbId, UserDAO},
|
||||||
message_utils::MessageTarget,
|
message_utils::MessageTarget,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn find_or_create_db_user_from_message(
|
pub async fn find_or_create_db_user_from_update(
|
||||||
db_pool: SqlitePool,
|
user_dao: UserDAO,
|
||||||
message: Message,
|
update: Update,
|
||||||
) -> Option<PersistedUser> {
|
) -> Option<PersistedUser> {
|
||||||
let user = message.from?;
|
let user = update.from()?.clone();
|
||||||
find_or_create_db_user(db_pool, user).await
|
find_or_create_db_user(user_dao, user).await
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn find_or_create_db_user_from_callback_query(
|
|
||||||
db_pool: SqlitePool,
|
|
||||||
callback_query: CallbackQuery,
|
|
||||||
) -> Option<PersistedUser> {
|
|
||||||
let user = callback_query.from;
|
|
||||||
find_or_create_db_user(db_pool, user).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_or_create_db_user(
|
pub async fn find_or_create_db_user(
|
||||||
db_pool: SqlitePool,
|
user_dao: UserDAO,
|
||||||
user: teloxide::types::User,
|
user: teloxide::types::User,
|
||||||
) -> Option<PersistedUser> {
|
) -> Option<PersistedUser> {
|
||||||
match UserDAO::find_or_create_by_telegram_user(&db_pool, user).await {
|
match user_dao.find_or_create_by_telegram_user(user).await {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
log::debug!("loaded user from db: {user:?}");
|
log::debug!("loaded user from db: {user:?}");
|
||||||
Some(user)
|
Some(user)
|
||||||
@@ -38,10 +30,31 @@ pub async fn find_or_create_db_user(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn message_into_message_target(message: Message) -> MessageTarget {
|
pub async fn find_listing_by_id(
|
||||||
message.chat.id.into()
|
listing_dao: ListingDAO,
|
||||||
|
listing_id: ListingDbId,
|
||||||
|
) -> Option<PersistedListing> {
|
||||||
|
listing_dao.find_by_id(listing_id).await.unwrap_or(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn callback_query_into_message_target(callback_query: CallbackQuery) -> Option<MessageTarget> {
|
pub fn update_into_message_target(update: Update) -> Option<MessageTarget> {
|
||||||
(&callback_query).try_into().ok()
|
match update.kind {
|
||||||
|
teloxide::types::UpdateKind::Message(message) => Some(message.chat.into()),
|
||||||
|
teloxide::types::UpdateKind::InlineQuery(inline_query) => Some(inline_query.from.into()),
|
||||||
|
teloxide::types::UpdateKind::CallbackQuery(callback_query) => {
|
||||||
|
(&callback_query).try_into().ok()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("Received unexpected update kind: {update:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pub fn message_into_message_target(message: Message) -> MessageTarget {
|
||||||
|
// message.chat.id.into()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn callback_query_into_message_target(callback_query: CallbackQuery) -> Option<MessageTarget> {
|
||||||
|
// (&callback_query).try_into().ok()
|
||||||
|
// }
|
||||||
|
|||||||
47
src/main.rs
47
src/main.rs
@@ -1,3 +1,4 @@
|
|||||||
|
mod bidding;
|
||||||
mod bot_result;
|
mod bot_result;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
@@ -8,20 +9,20 @@ mod handler_utils;
|
|||||||
mod keyboard_utils;
|
mod keyboard_utils;
|
||||||
mod message_utils;
|
mod message_utils;
|
||||||
mod sqlite_storage;
|
mod sqlite_storage;
|
||||||
|
mod start_command_data;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_utils;
|
mod test_utils;
|
||||||
mod wrap_endpoint;
|
mod wrap_endpoint;
|
||||||
|
|
||||||
use crate::handle_error::with_error_handler;
|
use crate::bidding::{bidding_handler, BiddingState};
|
||||||
use crate::handler_utils::{callback_query_into_message_target, message_into_message_target};
|
use crate::commands::{
|
||||||
use crate::sqlite_storage::SqliteStorage;
|
my_listings::{my_listings_handler, my_listings_inline_handler, MyListingsState},
|
||||||
use crate::{
|
new_listing::{new_listing_handler, NewListingState},
|
||||||
commands::{
|
|
||||||
my_listings::{my_listings_handler, my_listings_inline_handler, MyListingsState},
|
|
||||||
new_listing::{new_listing_handler, NewListingState},
|
|
||||||
},
|
|
||||||
handler_utils::find_or_create_db_user_from_callback_query,
|
|
||||||
};
|
};
|
||||||
|
use crate::db::{ListingDAO, UserDAO};
|
||||||
|
use crate::handle_error::with_error_handler;
|
||||||
|
use crate::handler_utils::{find_or_create_db_user_from_update, update_into_message_target};
|
||||||
|
use crate::sqlite_storage::SqliteStorage;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
pub use bot_result::*;
|
pub use bot_result::*;
|
||||||
use commands::*;
|
use commands::*;
|
||||||
@@ -73,6 +74,7 @@ enum DialogueRootState {
|
|||||||
MainMenu,
|
MainMenu,
|
||||||
NewListing(NewListingState),
|
NewListing(NewListingState),
|
||||||
MyListings(MyListingsState),
|
MyListings(MyListingsState),
|
||||||
|
Bidding(BiddingState),
|
||||||
}
|
}
|
||||||
|
|
||||||
type RootDialogue = Dialogue<DialogueRootState, SqliteStorage<Json>>;
|
type RootDialogue = Dialogue<DialogueRootState, SqliteStorage<Json>>;
|
||||||
@@ -97,24 +99,23 @@ async fn main() -> Result<()> {
|
|||||||
Dispatcher::builder(
|
Dispatcher::builder(
|
||||||
bot,
|
bot,
|
||||||
dptree::entry()
|
dptree::entry()
|
||||||
|
.filter_map(update_into_message_target)
|
||||||
|
.filter_map_async(find_or_create_db_user_from_update)
|
||||||
.branch(my_listings_inline_handler())
|
.branch(my_listings_inline_handler())
|
||||||
.branch(
|
.branch(
|
||||||
dptree::entry()
|
dptree::entry()
|
||||||
.enter_dialogue::<Update, SqliteStorage<Json>, DialogueRootState>()
|
.enter_dialogue::<Update, SqliteStorage<Json>, DialogueRootState>()
|
||||||
.branch(new_listing_handler())
|
.branch(new_listing_handler())
|
||||||
.branch(my_listings_handler())
|
.branch(my_listings_handler())
|
||||||
|
.branch(bidding_handler())
|
||||||
.branch(
|
.branch(
|
||||||
Update::filter_callback_query()
|
Update::filter_callback_query().branch(
|
||||||
.filter_map(callback_query_into_message_target)
|
dptree::case![DialogueRootState::MainMenu]
|
||||||
.branch(
|
.endpoint(with_error_handler(handle_main_menu_callback)),
|
||||||
dptree::case![DialogueRootState::MainMenu]
|
),
|
||||||
.filter_map_async(find_or_create_db_user_from_callback_query)
|
|
||||||
.endpoint(with_error_handler(handle_main_menu_callback)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
Update::filter_message()
|
Update::filter_message()
|
||||||
.map(message_into_message_target)
|
|
||||||
.filter_command::<Command>()
|
.filter_command::<Command>()
|
||||||
.branch(
|
.branch(
|
||||||
dptree::case![Command::Start]
|
dptree::case![Command::Start]
|
||||||
@@ -134,13 +135,13 @@ async fn main() -> Result<()> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(Update::filter_message().endpoint(with_error_handler(unknown_message_handler))),
|
||||||
Update::filter_message()
|
|
||||||
.map(message_into_message_target)
|
|
||||||
.endpoint(with_error_handler(unknown_message_handler)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.dependencies(dptree::deps![db_pool, dialog_storage])
|
.dependencies(dptree::deps![
|
||||||
|
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()
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ pub struct MessageTarget {
|
|||||||
pub message_id: Option<MessageId>,
|
pub message_id: Option<MessageId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MessageTarget {
|
||||||
|
pub fn only_chat_id(self) -> MessageTarget {
|
||||||
|
MessageTarget {
|
||||||
|
chat_id: self.chat_id,
|
||||||
|
message_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<ChatId> for MessageTarget {
|
impl From<ChatId> for MessageTarget {
|
||||||
fn from(val: ChatId) -> Self {
|
fn from(val: ChatId) -> Self {
|
||||||
MessageTarget {
|
MessageTarget {
|
||||||
@@ -120,6 +129,7 @@ pub async fn send_message(
|
|||||||
keyboard: Option<InlineKeyboardMarkup>,
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
if let Some(message_id) = target.message_id {
|
if let Some(message_id) = target.message_id {
|
||||||
|
log::info!("Editing message in chat: {target:?}");
|
||||||
let mut message = bot
|
let mut message = bot
|
||||||
.edit_message_text(target.chat_id, message_id, text.as_ref())
|
.edit_message_text(target.chat_id, message_id, text.as_ref())
|
||||||
.parse_mode(ParseMode::Html);
|
.parse_mode(ParseMode::Html);
|
||||||
@@ -128,6 +138,7 @@ pub async fn send_message(
|
|||||||
}
|
}
|
||||||
message.await.context("failed to edit message")?;
|
message.await.context("failed to edit message")?;
|
||||||
} else {
|
} else {
|
||||||
|
log::info!("Sending message to chat: {target:?}");
|
||||||
let mut message = bot
|
let mut message = bot
|
||||||
.send_message(target.chat_id, text.as_ref())
|
.send_message(target.chat_id, text.as_ref())
|
||||||
.parse_mode(ParseMode::Html);
|
.parse_mode(ParseMode::Html);
|
||||||
|
|||||||
75
src/start_command_data.rs
Normal file
75
src/start_command_data.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
||||||
|
use log::info;
|
||||||
|
use teloxide::types::{MediaKind, MessageKind, UpdateKind};
|
||||||
|
|
||||||
|
use crate::db::ListingDbId;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum StartCommandData {
|
||||||
|
PlaceBidOnListing(ListingDbId),
|
||||||
|
ViewListingDetails(ListingDbId),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StartCommandData> for String {
|
||||||
|
fn from(value: StartCommandData) -> Self {
|
||||||
|
match value {
|
||||||
|
StartCommandData::PlaceBidOnListing(listing_id) => {
|
||||||
|
format!("place_bid_on_listing:{listing_id}")
|
||||||
|
}
|
||||||
|
StartCommandData::ViewListingDetails(listing_id) => {
|
||||||
|
format!("view_listing_details:{listing_id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StartCommandData {
|
||||||
|
pub fn get_from_update(update: teloxide::types::Update) -> Option<StartCommandData> {
|
||||||
|
let message = match update.kind {
|
||||||
|
UpdateKind::Message(message) => Some(message),
|
||||||
|
_ => None,
|
||||||
|
}?;
|
||||||
|
let message = match message.kind {
|
||||||
|
MessageKind::Common(message) => Some(message),
|
||||||
|
_ => None,
|
||||||
|
}?;
|
||||||
|
let message = match message.media_kind {
|
||||||
|
MediaKind::Text(media_text) => Some(media_text),
|
||||||
|
_ => None,
|
||||||
|
}?;
|
||||||
|
let message = message.text.strip_prefix("/start ")?;
|
||||||
|
let decoded = BASE64_URL_SAFE.decode(message).ok()?;
|
||||||
|
let decoded = String::from_utf8(decoded).ok()?;
|
||||||
|
let parts = decoded.split(":").map(|s| s.trim()).collect::<Vec<&str>>();
|
||||||
|
info!("command parts: {parts:?}");
|
||||||
|
match parts.first()?.trim() {
|
||||||
|
"place_bid_on_listing" => Some(StartCommandData::PlaceBidOnListing(ListingDbId::new(
|
||||||
|
parts.get(1)?.parse::<i64>().ok()?,
|
||||||
|
))),
|
||||||
|
"view_listing_details" => Some(StartCommandData::ViewListingDetails(ListingDbId::new(
|
||||||
|
parts.get(1)?.parse::<i64>().ok()?,
|
||||||
|
))),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_place_bid_on_listing_start_command(
|
||||||
|
command: StartCommandData,
|
||||||
|
) -> Option<ListingDbId> {
|
||||||
|
if let StartCommandData::PlaceBidOnListing(listing_id) = command {
|
||||||
|
Some(listing_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_view_listing_details_start_command(
|
||||||
|
command: StartCommandData,
|
||||||
|
) -> Option<ListingDbId> {
|
||||||
|
if let StartCommandData::ViewListingDetails(listing_id) = command {
|
||||||
|
Some(listing_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
//! Test utilities including timestamp comparison macros
|
//! Test utilities including timestamp comparison macros
|
||||||
|
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
/// Assert that two timestamps are approximately equal within a given epsilon tolerance.
|
/// Assert that two timestamps are approximately equal within a given epsilon tolerance.
|
||||||
///
|
///
|
||||||
/// This macro is useful for testing timestamps that may have small variations due to
|
/// This macro is useful for testing timestamps that may have small variations due to
|
||||||
@@ -87,6 +89,20 @@ macro_rules! assert_timestamps_approx_eq_default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_test_pool() -> SqlitePool {
|
||||||
|
let pool = SqlitePool::connect("sqlite::memory:")
|
||||||
|
.await
|
||||||
|
.expect("Failed to create in-memory database");
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
sqlx::migrate!("./migrations")
|
||||||
|
.run(&pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to run database migrations");
|
||||||
|
|
||||||
|
pool
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ generate_wrapped!([T1, T2, T3], []);
|
|||||||
generate_wrapped!([T1, T2, T3, T4], []);
|
generate_wrapped!([T1, T2, T3, T4], []);
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5], []);
|
generate_wrapped!([T1, T2, T3, T4, T5], []);
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5, T6], []);
|
generate_wrapped!([T1, T2, T3, T4, T5, T6], []);
|
||||||
|
generate_wrapped!([T1, T2, T3, T4, T5, T6, T7], []);
|
||||||
|
|
||||||
generate_wrapped!([], [E1]);
|
generate_wrapped!([], [E1]);
|
||||||
generate_wrapped!([T1], [E1]);
|
generate_wrapped!([T1], [E1]);
|
||||||
@@ -83,6 +84,7 @@ generate_wrapped!([T1, T2, T3], [E1]);
|
|||||||
generate_wrapped!([T1, T2, T3, T4], [E1]);
|
generate_wrapped!([T1, T2, T3, T4], [E1]);
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5], [E1]);
|
generate_wrapped!([T1, T2, T3, T4, T5], [E1]);
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5, T6], [E1]);
|
generate_wrapped!([T1, T2, T3, T4, T5, T6], [E1]);
|
||||||
|
generate_wrapped!([T1, T2, T3, T4, T5, T6, T7], [E1]);
|
||||||
|
|
||||||
generate_wrapped!([], [E1, E2]);
|
generate_wrapped!([], [E1, E2]);
|
||||||
generate_wrapped!([T1], [E1, E2]);
|
generate_wrapped!([T1], [E1, E2]);
|
||||||
@@ -91,6 +93,7 @@ generate_wrapped!([T1, T2, T3], [E1, E2]);
|
|||||||
generate_wrapped!([T1, T2, T3, T4], [E1, E2]);
|
generate_wrapped!([T1, T2, T3, T4], [E1, E2]);
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5], [E1, E2]);
|
generate_wrapped!([T1, T2, T3, T4, T5], [E1, E2]);
|
||||||
generate_wrapped!([T1, T2, T3, T4, T5, T6], [E1, E2]);
|
generate_wrapped!([T1, T2, T3, T4, T5, T6], [E1, E2]);
|
||||||
|
generate_wrapped!([T1, T2, T3, T4, T5, T6, T7], [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