mocakble message sender trait
This commit is contained in:
71
Cargo.lock
generated
71
Cargo.lock
generated
@@ -529,6 +529,12 @@ version = "0.15.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "downcast"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dptree"
|
name = "dptree"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -678,6 +684,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fragile"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "funty"
|
name = "funty"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -1396,6 +1408,32 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mockall"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"downcast",
|
||||||
|
"fragile",
|
||||||
|
"mockall_derive",
|
||||||
|
"predicates",
|
||||||
|
"predicates-tree",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mockall_derive"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@@ -1625,6 +1663,7 @@ dependencies = [
|
|||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
"mockall",
|
||||||
"num",
|
"num",
|
||||||
"paste",
|
"paste",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -1752,6 +1791,32 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates"
|
||||||
|
version = "3.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"predicates-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates-core"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates-tree"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
|
||||||
|
dependencies = [
|
||||||
|
"predicates-core",
|
||||||
|
"termtree",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.3.0"
|
version = "3.3.0"
|
||||||
@@ -2819,6 +2884,12 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termtree"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.16"
|
version = "2.0.16"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ 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"
|
base64 = "0.22.1"
|
||||||
|
mockall = "0.13.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = "0.26.1"
|
rstest = "0.26.1"
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ use crate::{
|
|||||||
bid::NewBid,
|
bid::NewBid,
|
||||||
listing::{ListingFields, PersistedListing},
|
listing::{ListingFields, PersistedListing},
|
||||||
user::PersistedUser,
|
user::PersistedUser,
|
||||||
BidDAO, ListingDbId, MoneyAmount, UserDAO,
|
ListingDbId, MoneyAmount, UserDAO,
|
||||||
},
|
},
|
||||||
dptree_utils::MapTwo,
|
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::{MessageTarget, SendHtmlMessage},
|
message_utils::MessageTarget,
|
||||||
start_command_data::StartCommandData,
|
start_command_data::StartCommandData,
|
||||||
App, BotError, BotHandler, BotResult, DialogueRootState, RootDialogue,
|
App, BotError, BotHandler, BotResult, DialogueRootState, RootDialogue,
|
||||||
};
|
};
|
||||||
@@ -173,7 +173,6 @@ async fn handle_awaiting_confirm_bid_amount_callback(
|
|||||||
app: App,
|
app: App,
|
||||||
listing: PersistedListing,
|
listing: PersistedListing,
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
bid_dao: BidDAO,
|
|
||||||
bid_amount: MoneyAmount,
|
bid_amount: MoneyAmount,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
@@ -189,7 +188,7 @@ async fn handle_awaiting_confirm_bid_amount_callback(
|
|||||||
"cancel_bid" => {
|
"cancel_bid" => {
|
||||||
dialogue.exit().await.context("failed to exit dialogue")?;
|
dialogue.exit().await.context("failed to exit dialogue")?;
|
||||||
app.bot
|
app.bot
|
||||||
.send_html_message(target, "Bid cancelled", None)
|
.send_html_message(target, "Bid cancelled".to_string(), None)
|
||||||
.await?;
|
.await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -201,7 +200,7 @@ async fn handle_awaiting_confirm_bid_amount_callback(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let bid = NewBid::new_basic(listing.persisted.id, user.persisted.id, bid_amount);
|
let bid = NewBid::new_basic(listing.persisted.id, user.persisted.id, bid_amount);
|
||||||
bid_dao.insert_bid(bid).await?;
|
app.daos.bid.insert_bid(bid).await?;
|
||||||
|
|
||||||
dialogue.exit().await.context("failed to exit dialogue")?;
|
dialogue.exit().await.context("failed to exit dialogue")?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{message_utils::{MessageTarget, SendHtmlMessage}, App, BotResult, Command};
|
use crate::{message_utils::MessageTarget, App, BotResult, Command};
|
||||||
use teloxide::utils::command::BotCommands;
|
use teloxide::utils::command::BotCommands;
|
||||||
|
|
||||||
pub async fn handle_help(app: App, target: MessageTarget) -> BotResult {
|
pub async fn handle_help(app: App, target: MessageTarget) -> BotResult {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{message_utils::{MessageTarget, SendHtmlMessage}, App, BotResult};
|
use crate::{message_utils::MessageTarget, App, BotResult};
|
||||||
use log::info;
|
use log::info;
|
||||||
use teloxide::types::Message;
|
use teloxide::types::Message;
|
||||||
|
|
||||||
@@ -9,7 +9,8 @@ pub async fn handle_my_bids(app: App, msg: Message, target: MessageTarget) -> Bo
|
|||||||
• Bid status (winning/outbid)\n\
|
• Bid status (winning/outbid)\n\
|
||||||
• Proxy bid settings\n\
|
• Proxy bid settings\n\
|
||||||
• Auction end times\n\n\
|
• Auction end times\n\n\
|
||||||
Feature in development! 🏗️";
|
Feature in development! 🏗️"
|
||||||
|
.to_string();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"User {} ({}) checked their bids",
|
"User {} ({}) checked their bids",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
mod keyboard;
|
mod keyboard;
|
||||||
|
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
case,
|
case,
|
||||||
commands::{
|
commands::{
|
||||||
@@ -17,7 +19,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
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, MessageTarget, SendHtmlMessage},
|
message_utils::{extract_callback_data, pluralize_with_count, MessageTarget},
|
||||||
start_command_data::StartCommandData,
|
start_command_data::StartCommandData,
|
||||||
App, BotError, BotResult, Command, DialogueRootState, RootDialogue,
|
App, BotError, BotResult, Command, DialogueRootState, RootDialogue,
|
||||||
};
|
};
|
||||||
@@ -32,7 +34,6 @@ use teloxide::{
|
|||||||
InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResult, InlineQueryResultArticle,
|
InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResult, InlineQueryResultArticle,
|
||||||
InputMessageContent, InputMessageContentText, ParseMode, User,
|
InputMessageContent, InputMessageContentText, ParseMode, User,
|
||||||
},
|
},
|
||||||
Bot,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
@@ -184,7 +185,7 @@ async fn handle_forward_listing(
|
|||||||
app.bot
|
app.bot
|
||||||
.answer_inline_query(
|
.answer_inline_query(
|
||||||
inline_query.id,
|
inline_query.id,
|
||||||
[InlineQueryResult::Article(
|
vec![InlineQueryResult::Article(
|
||||||
InlineQueryResultArticle::new(
|
InlineQueryResultArticle::new(
|
||||||
listing.persisted.id.to_string(),
|
listing.persisted.id.to_string(),
|
||||||
format!("💰 {} - ${}", listing.base.title, current_price),
|
format!("💰 {} - ${}", listing.base.title, current_price),
|
||||||
@@ -264,7 +265,8 @@ pub async fn enter_my_listings(
|
|||||||
.send_html_message(
|
.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."
|
||||||
|
.to_string(),
|
||||||
Some(keyboard),
|
Some(keyboard),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -295,7 +297,7 @@ async fn handle_viewing_listings_callback(
|
|||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
let data = extract_callback_data(&app.bot, callback_query).await?;
|
let data = extract_callback_data(app.bot.deref(), 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(app, dialogue, target).await;
|
return enter_main_menu(app, dialogue, target).await;
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ pub async fn handle_selecting_listing_type_callback(
|
|||||||
pub async fn handle_awaiting_draft_field_callback(
|
pub async fn handle_awaiting_draft_field_callback(
|
||||||
app: App,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
(field, draft): (ListingField, ListingDraft),
|
field: ListingField,
|
||||||
|
draft: ListingDraft,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
@@ -182,7 +183,7 @@ async fn handle_slots_callback(
|
|||||||
app.bot
|
app.bot
|
||||||
.send_html_message(
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
&response,
|
response,
|
||||||
get_keyboard_for_field(ListingField::StartTime),
|
get_keyboard_for_field(ListingField::StartTime),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -217,7 +218,7 @@ async fn handle_start_time_callback(
|
|||||||
app.bot
|
app.bot
|
||||||
.send_html_message(
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
&response,
|
response,
|
||||||
get_keyboard_for_field(ListingField::EndTime),
|
get_keyboard_for_field(ListingField::EndTime),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -304,7 +305,7 @@ async fn handle_currency_type_callback(
|
|||||||
);
|
);
|
||||||
transition_to_field(dialogue, next_field, draft).await?;
|
transition_to_field(dialogue, next_field, draft).await?;
|
||||||
app.bot
|
app.bot
|
||||||
.send_html_message(target, &response, get_keyboard_for_field(next_field))
|
.send_html_message(target, response, get_keyboard_for_field(next_field))
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
use super::{callbacks::*, handlers::*, types::*};
|
use super::{callbacks::*, handlers::*, types::*};
|
||||||
use crate::{case, handle_error::with_error_handler, BotHandler, Command, DialogueRootState};
|
use crate::{
|
||||||
|
dptree_utils::{identity, MapTwo},
|
||||||
|
handle_error::with_error_handler,
|
||||||
|
BotHandler, Command, DialogueRootState,
|
||||||
|
};
|
||||||
|
use dptree::case;
|
||||||
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,49 +19,44 @@ pub fn new_listing_handler() -> BotHandler {
|
|||||||
.endpoint(with_error_handler(handle_new_listing_command)),
|
.endpoint(with_error_handler(handle_new_listing_command)),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
dptree::entry()
|
||||||
NewListingState::AwaitingDraftField { field, draft }
|
.chain(case![DialogueRootState::NewListing(state)])
|
||||||
)]
|
.branch(
|
||||||
.endpoint(with_error_handler(handle_awaiting_draft_field_input)),
|
case![NewListingState::AwaitingDraftField { field, draft }]
|
||||||
)
|
.map2(identity::<(ListingField, ListingDraft)>)
|
||||||
.branch(
|
.endpoint(with_error_handler(handle_awaiting_draft_field_input)),
|
||||||
case![DialogueRootState::NewListing(
|
)
|
||||||
NewListingState::EditingDraftField { field, draft }
|
.branch(
|
||||||
)]
|
case![NewListingState::EditingDraftField { field, draft }]
|
||||||
.endpoint(with_error_handler(handle_editing_field_input)),
|
.map2(identity::<(ListingField, ListingDraft)>)
|
||||||
|
.endpoint(with_error_handler(handle_editing_field_input)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
Update::filter_callback_query()
|
Update::filter_callback_query()
|
||||||
|
.chain(case![DialogueRootState::NewListing(state)])
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![NewListingState::SelectingListingType]
|
||||||
NewListingState::SelectingListingType
|
.endpoint(with_error_handler(handle_selecting_listing_type_callback)),
|
||||||
)]
|
|
||||||
.endpoint(with_error_handler(handle_selecting_listing_type_callback)),
|
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![NewListingState::AwaitingDraftField { field, draft }]
|
||||||
NewListingState::AwaitingDraftField { field, draft }
|
.map2(identity::<(ListingField, ListingDraft)>)
|
||||||
)]
|
.endpoint(with_error_handler(handle_awaiting_draft_field_callback)),
|
||||||
.endpoint(with_error_handler(handle_awaiting_draft_field_callback)),
|
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![NewListingState::ViewingDraft(draft)]
|
||||||
NewListingState::ViewingDraft(draft)
|
.endpoint(with_error_handler(handle_viewing_draft_callback)),
|
||||||
)]
|
|
||||||
.endpoint(with_error_handler(handle_viewing_draft_callback)),
|
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![NewListingState::EditingDraft(draft)]
|
||||||
NewListingState::EditingDraft(draft)
|
.endpoint(with_error_handler(handle_editing_draft_callback)),
|
||||||
)]
|
|
||||||
.endpoint(with_error_handler(handle_editing_draft_callback)),
|
|
||||||
)
|
)
|
||||||
.branch(
|
.branch(
|
||||||
case![DialogueRootState::NewListing(
|
case![NewListingState::EditingDraftField { field, draft }]
|
||||||
NewListingState::EditingDraftField { field, draft }
|
.map2(identity::<(ListingField, ListingDraft)>)
|
||||||
)]
|
.endpoint(with_error_handler(handle_editing_draft_field_callback)),
|
||||||
.endpoint(with_error_handler(handle_editing_draft_field_callback)),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ pub async fn enter_select_new_listing_type(
|
|||||||
app.bot
|
app.bot
|
||||||
.send_html_message(
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
get_listing_type_selection_message(),
|
get_listing_type_selection_message().to_string(),
|
||||||
Some(get_listing_type_keyboard()),
|
Some(get_listing_type_keyboard()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -69,7 +69,8 @@ pub async fn enter_select_new_listing_type(
|
|||||||
pub async fn handle_awaiting_draft_field_input(
|
pub async fn handle_awaiting_draft_field_input(
|
||||||
app: App,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
(field, mut draft): (ListingField, ListingDraft),
|
field: ListingField,
|
||||||
|
mut draft: ListingDraft,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
msg: Message,
|
msg: Message,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
@@ -111,7 +112,8 @@ pub async fn handle_awaiting_draft_field_input(
|
|||||||
pub async fn handle_editing_field_input(
|
pub async fn handle_editing_field_input(
|
||||||
app: App,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
(field, mut draft): (ListingField, ListingDraft),
|
field: ListingField,
|
||||||
|
mut draft: ListingDraft,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
msg: Message,
|
msg: Message,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
@@ -156,8 +158,9 @@ pub async fn handle_viewing_draft_callback(
|
|||||||
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."
|
||||||
app.bot.send_html_message(target, &response, None).await?;
|
.to_string();
|
||||||
|
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 => {
|
||||||
@@ -165,8 +168,9 @@ 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."
|
||||||
app.bot.send_html_message(target, &response, None).await?;
|
.to_string();
|
||||||
|
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 => {
|
||||||
@@ -228,7 +232,8 @@ pub async fn handle_editing_draft_callback(
|
|||||||
pub async fn handle_editing_draft_field_callback(
|
pub async fn handle_editing_draft_field_callback(
|
||||||
app: App,
|
app: App,
|
||||||
dialogue: RootDialogue,
|
dialogue: RootDialogue,
|
||||||
(field, draft): (ListingField, ListingDraft),
|
field: ListingField,
|
||||||
|
draft: ListingDraft,
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
) -> BotResult {
|
) -> BotResult {
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
use crate::{
|
use crate::{message_utils::MessageTarget, App, BotResult};
|
||||||
message_utils::{MessageTarget, SendHtmlMessage},
|
|
||||||
App, BotResult,
|
|
||||||
};
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use teloxide::types::Message;
|
use teloxide::types::Message;
|
||||||
|
|
||||||
@@ -12,7 +9,8 @@ pub async fn handle_settings(app: App, msg: Message, target: MessageTarget) -> B
|
|||||||
• 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! 🛠️"
|
||||||
|
.to_string();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"User {} ({}) accessed settings",
|
"User {} ({}) accessed settings",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
commands::my_listings::enter_my_listings,
|
commands::my_listings::enter_my_listings,
|
||||||
db::user::PersistedUser,
|
db::user::PersistedUser,
|
||||||
keyboard_buttons,
|
keyboard_buttons,
|
||||||
message_utils::{extract_callback_data, MessageTarget, SendHtmlMessage as _},
|
message_utils::{extract_callback_data, MessageTarget},
|
||||||
App, BotResult, Command, DialogueRootState, RootDialogue,
|
App, BotResult, Command, DialogueRootState, RootDialogue,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ pub async fn enter_main_menu(app: App, dialogue: RootDialogue, target: MessageTa
|
|||||||
app.bot
|
app.bot
|
||||||
.send_html_message(
|
.send_html_message(
|
||||||
target,
|
target,
|
||||||
get_main_menu_message(),
|
get_main_menu_message().to_string(),
|
||||||
Some(MainMenuButtons::to_keyboard()),
|
Some(MainMenuButtons::to_keyboard()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -92,7 +92,8 @@ pub async fn handle_main_menu_callback(
|
|||||||
• 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! 🛠️"
|
||||||
|
.to_string(),
|
||||||
Some(MainMenuButtons::to_keyboard()),
|
Some(MainMenuButtons::to_keyboard()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -107,7 +108,8 @@ pub async fn handle_main_menu_callback(
|
|||||||
• 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! 🛠️"
|
||||||
|
.to_string(),
|
||||||
Some(MainMenuButtons::to_keyboard()),
|
Some(MainMenuButtons::to_keyboard()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn identity<T>(t: T) -> T {
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::ops::ControlFlow;
|
use std::ops::ControlFlow;
|
||||||
|
|||||||
@@ -1,40 +1,41 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
message_utils::{MessageTarget, SendHtmlMessage},
|
message_utils::MessageTarget, wrap_endpoint, App, BotError, BotResult, WrappedAsyncFn,
|
||||||
wrap_endpoint, BotError, BotResult, WrappedAsyncFn,
|
|
||||||
};
|
};
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use teloxide::Bot;
|
|
||||||
|
|
||||||
pub async fn handle_error(bot: Bot, target: MessageTarget, error: BotError) -> BotResult {
|
pub async fn handle_error(app: App, target: MessageTarget, error: BotError) -> BotResult {
|
||||||
log::error!("Error in handler: {error:?}");
|
log::error!("Error in handler: {error:?}");
|
||||||
match error {
|
match error {
|
||||||
BotError::UserVisibleError(message) => bot.send_html_message(target, message, None).await?,
|
BotError::UserVisibleError(message) => {
|
||||||
|
app.bot.send_html_message(target, message, None).await?
|
||||||
|
}
|
||||||
BotError::InternalError(_) => {
|
BotError::InternalError(_) => {
|
||||||
bot.send_html_message(
|
app.bot
|
||||||
target,
|
.send_html_message(
|
||||||
"An internal error occurred. Please try again later.",
|
target,
|
||||||
None,
|
"An internal error occurred. Please try again later.".to_string(),
|
||||||
)
|
None,
|
||||||
.await?;
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn boxed_handle_error(
|
fn boxed_handle_error(
|
||||||
bot: Bot,
|
app: App,
|
||||||
target: MessageTarget,
|
target: MessageTarget,
|
||||||
error: BotError,
|
error: BotError,
|
||||||
) -> BoxFuture<'static, BotResult> {
|
) -> BoxFuture<'static, BotResult> {
|
||||||
Box::pin(handle_error(bot, target, error))
|
Box::pin(handle_error(app, target, error))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ErrorHandlerWrapped<FnBase, FnBaseArgs> = WrappedAsyncFn<
|
pub type ErrorHandlerWrapped<FnBase, FnBaseArgs> = WrappedAsyncFn<
|
||||||
FnBase,
|
FnBase,
|
||||||
fn(Bot, MessageTarget, BotError) -> BoxFuture<'static, BotResult>,
|
fn(App, MessageTarget, BotError) -> BoxFuture<'static, BotResult>,
|
||||||
BotError,
|
BotError,
|
||||||
FnBaseArgs,
|
FnBaseArgs,
|
||||||
(Bot, MessageTarget),
|
(App, MessageTarget),
|
||||||
>;
|
>;
|
||||||
|
|
||||||
pub fn with_error_handler<FnBase, FnBaseArgs>(
|
pub fn with_error_handler<FnBase, FnBaseArgs>(
|
||||||
|
|||||||
141
src/main.rs
141
src/main.rs
@@ -7,6 +7,7 @@ mod dptree_utils;
|
|||||||
mod handle_error;
|
mod handle_error;
|
||||||
mod handler_utils;
|
mod handler_utils;
|
||||||
mod keyboard_utils;
|
mod keyboard_utils;
|
||||||
|
mod message_sender;
|
||||||
mod message_utils;
|
mod message_utils;
|
||||||
mod sqlite_storage;
|
mod sqlite_storage;
|
||||||
mod start_command_data;
|
mod start_command_data;
|
||||||
@@ -14,6 +15,8 @@ mod start_command_data;
|
|||||||
mod test_utils;
|
mod test_utils;
|
||||||
mod wrap_endpoint;
|
mod wrap_endpoint;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::bidding::{bidding_handler, BiddingState};
|
use crate::bidding::{bidding_handler, BiddingState};
|
||||||
use crate::commands::{
|
use crate::commands::{
|
||||||
my_listings::{my_listings_handler, my_listings_inline_handler, MyListingsState},
|
my_listings::{my_listings_handler, my_listings_inline_handler, MyListingsState},
|
||||||
@@ -22,6 +25,7 @@ use crate::commands::{
|
|||||||
use crate::db::DAOs;
|
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::message_sender::BoxMessageSender;
|
||||||
use crate::sqlite_storage::SqliteStorage;
|
use crate::sqlite_storage::SqliteStorage;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
pub use bot_result::*;
|
pub use bot_result::*;
|
||||||
@@ -35,13 +39,16 @@ pub use wrap_endpoint::*;
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub bot: Bot,
|
pub bot: Arc<BoxMessageSender>,
|
||||||
pub daos: DAOs,
|
pub daos: DAOs,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(bot: Bot, daos: DAOs) -> Self {
|
pub fn new(bot: BoxMessageSender, daos: DAOs) -> Self {
|
||||||
Self { bot, daos }
|
Self {
|
||||||
|
bot: Arc::new(bot),
|
||||||
|
daos,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +98,50 @@ enum DialogueRootState {
|
|||||||
|
|
||||||
type RootDialogue = Dialogue<DialogueRootState, SqliteStorage<Json>>;
|
type RootDialogue = Dialogue<DialogueRootState, SqliteStorage<Json>>;
|
||||||
|
|
||||||
|
pub fn main_handler() -> BotHandler {
|
||||||
|
dptree::entry()
|
||||||
|
.map(|app: App| app.daos.clone())
|
||||||
|
.map(|daos: DAOs| daos.user.clone())
|
||||||
|
.map(|daos: DAOs| daos.listing.clone())
|
||||||
|
.map(|daos: DAOs| daos.bid.clone())
|
||||||
|
.filter_map(update_into_message_target)
|
||||||
|
.filter_map_async(find_or_create_db_user_from_update)
|
||||||
|
.branch(my_listings_inline_handler())
|
||||||
|
.branch(
|
||||||
|
dptree::entry()
|
||||||
|
.enter_dialogue::<Update, SqliteStorage<Json>, DialogueRootState>()
|
||||||
|
.branch(new_listing_handler())
|
||||||
|
.branch(my_listings_handler())
|
||||||
|
.branch(bidding_handler())
|
||||||
|
.branch(
|
||||||
|
Update::filter_callback_query().branch(
|
||||||
|
dptree::case![DialogueRootState::MainMenu]
|
||||||
|
.endpoint(with_error_handler(handle_main_menu_callback)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.filter_command::<Command>()
|
||||||
|
.branch(
|
||||||
|
dptree::case![Command::Start]
|
||||||
|
.endpoint(with_error_handler(handle_start)),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
dptree::case![Command::Help].endpoint(with_error_handler(handle_help)),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
dptree::case![Command::MyBids]
|
||||||
|
.endpoint(with_error_handler(handle_my_bids)),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
dptree::case![Command::Settings]
|
||||||
|
.endpoint(with_error_handler(handle_settings)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.branch(Update::filter_message().endpoint(with_error_handler(unknown_message_handler)))
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
// Load and validate configuration from environment/.env file
|
// Load and validate configuration from environment/.env file
|
||||||
@@ -100,7 +151,7 @@ async fn main() -> Result<()> {
|
|||||||
let db_pool = config.create_database_pool().await?;
|
let db_pool = config.create_database_pool().await?;
|
||||||
|
|
||||||
info!("Starting Pawctioneer Bot...");
|
info!("Starting Pawctioneer Bot...");
|
||||||
let bot = Bot::new(&config.telegram_token);
|
let bot = Box::new(Bot::new(&config.telegram_token));
|
||||||
|
|
||||||
// Set up the bot's command menu
|
// Set up the bot's command menu
|
||||||
setup_bot_commands(&bot).await?;
|
setup_bot_commands(&bot).await?;
|
||||||
@@ -110,53 +161,20 @@ async fn main() -> Result<()> {
|
|||||||
let app = App::new(bot.clone(), daos.clone());
|
let app = App::new(bot.clone(), daos.clone());
|
||||||
|
|
||||||
// Create dispatcher with dialogue system
|
// Create dispatcher with dialogue system
|
||||||
Dispatcher::builder(
|
Dispatcher::builder(bot, main_handler())
|
||||||
bot,
|
.dependencies(dptree::deps![
|
||||||
dptree::entry()
|
dialog_storage,
|
||||||
.filter_map(update_into_message_target)
|
daos,
|
||||||
.filter_map_async(find_or_create_db_user_from_update)
|
app.daos.user.clone(),
|
||||||
.branch(my_listings_inline_handler())
|
app.daos.listing.clone(),
|
||||||
.branch(
|
app.daos.bid.clone(),
|
||||||
dptree::entry()
|
app
|
||||||
.enter_dialogue::<Update, SqliteStorage<Json>, DialogueRootState>()
|
])
|
||||||
.branch(new_listing_handler())
|
.enable_ctrlc_handler()
|
||||||
.branch(my_listings_handler())
|
.worker_queue_size(1)
|
||||||
.branch(bidding_handler())
|
.build()
|
||||||
.branch(
|
.dispatch()
|
||||||
Update::filter_callback_query().branch(
|
.await;
|
||||||
dptree::case![DialogueRootState::MainMenu]
|
|
||||||
.endpoint(with_error_handler(handle_main_menu_callback)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
.filter_command::<Command>()
|
|
||||||
.branch(
|
|
||||||
dptree::case![Command::Start]
|
|
||||||
.endpoint(with_error_handler(handle_start)),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
dptree::case![Command::Help]
|
|
||||||
.endpoint(with_error_handler(handle_help)),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
dptree::case![Command::MyBids]
|
|
||||||
.endpoint(with_error_handler(handle_my_bids)),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
dptree::case![Command::Settings]
|
|
||||||
.endpoint(with_error_handler(handle_settings)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.branch(Update::filter_message().endpoint(with_error_handler(unknown_message_handler))),
|
|
||||||
)
|
|
||||||
.dependencies(dptree::deps![dialog_storage, daos, app])
|
|
||||||
.enable_ctrlc_handler()
|
|
||||||
.worker_queue_size(1)
|
|
||||||
.build()
|
|
||||||
.dispatch()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -169,3 +187,24 @@ async fn unknown_message_handler(msg: Message) -> BotResult {
|
|||||||
msg.text().unwrap_or("")
|
msg.text().unwrap_or("")
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::message_sender::MockMessageSender;
|
||||||
|
use crate::test_utils::create_deps;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_main_handler() {
|
||||||
|
let mut bot = MockMessageSender::new();
|
||||||
|
bot.expect_send_html_message()
|
||||||
|
.times(1)
|
||||||
|
.returning(|_, _, _| Ok(()));
|
||||||
|
let deps = create_deps(bot).await;
|
||||||
|
let handler = main_handler();
|
||||||
|
dptree::type_check(handler.sig(), &deps, &[]);
|
||||||
|
|
||||||
|
let result = handler.dispatch(deps).await;
|
||||||
|
assert!(matches!(result, ControlFlow::Break(Ok(()))), "{:?}", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
109
src/message_sender.rs
Normal file
109
src/message_sender.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
use crate::{message_utils::MessageTarget, BotError, BotResult};
|
||||||
|
use anyhow::Context;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use teloxide::{
|
||||||
|
payloads::{EditMessageTextSetters, SendMessageSetters},
|
||||||
|
prelude::Requester,
|
||||||
|
types::{
|
||||||
|
CallbackQueryId, InlineKeyboardMarkup, InlineQueryId, InlineQueryResult, Me, ParseMode,
|
||||||
|
},
|
||||||
|
Bot,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait MessageSender {
|
||||||
|
async fn send_html_message(
|
||||||
|
&self,
|
||||||
|
target: MessageTarget,
|
||||||
|
text: String,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult;
|
||||||
|
async fn answer_inline_query(
|
||||||
|
&self,
|
||||||
|
inline_query_id: InlineQueryId,
|
||||||
|
results: Vec<InlineQueryResult>,
|
||||||
|
) -> BotResult;
|
||||||
|
async fn answer_callback_query(&self, query_id: CallbackQueryId) -> BotResult;
|
||||||
|
async fn get_me(&self) -> BotResult<Me>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type BoxMessageSender = Box<dyn MessageSender + Send + Sync>;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mockall::mock! {
|
||||||
|
pub MessageSender {}
|
||||||
|
impl Clone for MessageSender {
|
||||||
|
fn clone(&self) -> Self;
|
||||||
|
}
|
||||||
|
#[async_trait]
|
||||||
|
impl MessageSender for MessageSender {
|
||||||
|
async fn send_html_message(
|
||||||
|
&self,
|
||||||
|
target: MessageTarget,
|
||||||
|
text: String,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult;
|
||||||
|
async fn answer_inline_query(
|
||||||
|
&self,
|
||||||
|
inline_query_id: InlineQueryId,
|
||||||
|
results: Vec<InlineQueryResult>,
|
||||||
|
) -> BotResult;
|
||||||
|
async fn answer_callback_query(&self, query_id: CallbackQueryId) -> BotResult;
|
||||||
|
async fn get_me(&self) -> BotResult<Me>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MessageSender for Bot {
|
||||||
|
async fn send_html_message(
|
||||||
|
&self,
|
||||||
|
target: MessageTarget,
|
||||||
|
text: String,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult {
|
||||||
|
if let Some(message_id) = target.message_id {
|
||||||
|
log::info!("Editing message in chat: {target:?}");
|
||||||
|
let mut message = self
|
||||||
|
.edit_message_text(target.chat_id, message_id, &text)
|
||||||
|
.parse_mode(ParseMode::Html);
|
||||||
|
if let Some(kb) = keyboard {
|
||||||
|
message = message.reply_markup(kb);
|
||||||
|
}
|
||||||
|
message.await.context("failed to edit message")?;
|
||||||
|
} else {
|
||||||
|
log::info!("Sending message to chat: {target:?}");
|
||||||
|
let mut message = self
|
||||||
|
.send_message(target.chat_id, &text)
|
||||||
|
.parse_mode(ParseMode::Html);
|
||||||
|
if let Some(kb) = keyboard {
|
||||||
|
message = message.reply_markup(kb);
|
||||||
|
}
|
||||||
|
message.await.context("failed to send message")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn answer_inline_query(
|
||||||
|
&self,
|
||||||
|
inline_query_id: InlineQueryId,
|
||||||
|
results: Vec<InlineQueryResult>,
|
||||||
|
) -> BotResult {
|
||||||
|
teloxide::prelude::Requester::answer_inline_query(self, inline_query_id, results)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|err| BotError::InternalError(err.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn answer_callback_query(&self, query_id: CallbackQueryId) -> BotResult {
|
||||||
|
teloxide::prelude::Requester::answer_callback_query(self, query_id)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|err| BotError::InternalError(err.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_me(&self) -> BotResult<Me> {
|
||||||
|
teloxide::prelude::Requester::get_me(self)
|
||||||
|
.await
|
||||||
|
.map_err(|err| BotError::InternalError(err.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
use crate::BotResult;
|
use crate::{message_sender::BoxMessageSender, BotResult};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::anyhow;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use num::One;
|
use num::One;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use teloxide::{
|
use teloxide::types::{
|
||||||
payloads::{EditMessageTextSetters as _, SendMessageSetters as _},
|
CallbackQuery, Chat, ChatId, InlineKeyboardButton, InlineKeyboardMarkup,
|
||||||
prelude::Requester as _,
|
MaybeInaccessibleMessage, MessageId, User,
|
||||||
types::{
|
|
||||||
CallbackQuery, Chat, ChatId, InlineKeyboardButton, InlineKeyboardMarkup,
|
|
||||||
MaybeInaccessibleMessage, MessageId, ParseMode, User,
|
|
||||||
},
|
|
||||||
Bot,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -121,55 +116,6 @@ 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
|
|
||||||
async fn send_html_message(
|
|
||||||
bot: &Bot,
|
|
||||||
target: MessageTarget,
|
|
||||||
text: impl AsRef<str>,
|
|
||||||
keyboard: Option<InlineKeyboardMarkup>,
|
|
||||||
) -> BotResult {
|
|
||||||
if let Some(message_id) = target.message_id {
|
|
||||||
log::info!("Editing message in chat: {target:?}");
|
|
||||||
let mut message = bot
|
|
||||||
.edit_message_text(target.chat_id, message_id, text.as_ref())
|
|
||||||
.parse_mode(ParseMode::Html);
|
|
||||||
if let Some(kb) = keyboard {
|
|
||||||
message = message.reply_markup(kb);
|
|
||||||
}
|
|
||||||
message.await.context("failed to edit message")?;
|
|
||||||
} else {
|
|
||||||
log::info!("Sending message to chat: {target:?}");
|
|
||||||
let mut message = bot
|
|
||||||
.send_message(target.chat_id, text.as_ref())
|
|
||||||
.parse_mode(ParseMode::Html);
|
|
||||||
if let Some(kb) = keyboard {
|
|
||||||
message = message.reply_markup(kb);
|
|
||||||
}
|
|
||||||
message.await.context("failed to send message")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// KEYBOARD CREATION UTILITIES
|
// KEYBOARD CREATION UTILITIES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -180,7 +126,10 @@ pub fn create_single_button_keyboard(text: &str, callback_data: &str) -> InlineK
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract callback data and answer callback query
|
// Extract callback data and answer callback query
|
||||||
pub async fn extract_callback_data(bot: &Bot, callback_query: CallbackQuery) -> BotResult<String> {
|
pub async fn extract_callback_data(
|
||||||
|
bot: &BoxMessageSender,
|
||||||
|
callback_query: CallbackQuery,
|
||||||
|
) -> BotResult<String> {
|
||||||
let data = match callback_query.data {
|
let data = match callback_query.data {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => return Err(anyhow!("Missing data in callback query"))?,
|
None => return Err(anyhow!("Missing data in callback query"))?,
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
//! Test utilities including timestamp comparison macros
|
//! Test utilities including timestamp comparison macros
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use dptree::di::DependencyMap;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
use teloxide::dispatching::dialogue::serializer::Json;
|
||||||
|
use teloxide::types::*;
|
||||||
|
|
||||||
|
use crate::{db::DAOs, message_sender::MockMessageSender, sqlite_storage::SqliteStorage, App};
|
||||||
|
|
||||||
/// Assert that two timestamps are approximately equal within a given epsilon tolerance.
|
/// Assert that two timestamps are approximately equal within a given epsilon tolerance.
|
||||||
///
|
///
|
||||||
@@ -103,6 +109,88 @@ pub async fn create_test_pool() -> SqlitePool {
|
|||||||
pool
|
pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_tele_user(username: &str) -> User {
|
||||||
|
User {
|
||||||
|
id: UserId(1),
|
||||||
|
username: Some(username.to_string()),
|
||||||
|
first_name: username.to_string(),
|
||||||
|
last_name: Some("lastname".to_string()),
|
||||||
|
is_bot: false,
|
||||||
|
language_code: Some("en".to_string()),
|
||||||
|
is_premium: false,
|
||||||
|
added_to_attachment_menu: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_tele_private_chat(user: &User) -> Chat {
|
||||||
|
Chat {
|
||||||
|
id: ChatId(1),
|
||||||
|
kind: ChatKind::Private(ChatPrivate {
|
||||||
|
username: user.username.clone(),
|
||||||
|
first_name: Some(user.first_name.clone()),
|
||||||
|
last_name: user.last_name.clone(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_tele_update() -> Update {
|
||||||
|
let user = create_tele_user("sender");
|
||||||
|
let chat = create_tele_private_chat(&user);
|
||||||
|
Update {
|
||||||
|
id: UpdateId(1),
|
||||||
|
kind: UpdateKind::Message(Message {
|
||||||
|
id: MessageId(1),
|
||||||
|
thread_id: None,
|
||||||
|
from: Some(user),
|
||||||
|
sender_chat: None,
|
||||||
|
date: Utc::now(),
|
||||||
|
chat,
|
||||||
|
is_topic_message: false,
|
||||||
|
via_bot: None,
|
||||||
|
sender_business_bot: None,
|
||||||
|
kind: MessageKind::Common(MessageCommon {
|
||||||
|
media_kind: MediaKind::Text(MediaText {
|
||||||
|
text: "test".to_string(),
|
||||||
|
entities: vec![],
|
||||||
|
link_preview_options: None,
|
||||||
|
}),
|
||||||
|
author_signature: None,
|
||||||
|
paid_star_count: None,
|
||||||
|
effect_id: None,
|
||||||
|
forward_origin: None,
|
||||||
|
reply_to_message: None,
|
||||||
|
external_reply: None,
|
||||||
|
quote: None,
|
||||||
|
reply_to_story: None,
|
||||||
|
sender_boost_count: None,
|
||||||
|
edit_date: None,
|
||||||
|
reply_markup: None,
|
||||||
|
is_automatic_forward: false,
|
||||||
|
has_protected_content: false,
|
||||||
|
is_from_offline: false,
|
||||||
|
business_connection_id: None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_deps(mock_bot: MockMessageSender) -> DependencyMap {
|
||||||
|
let update = create_tele_update();
|
||||||
|
let pool = create_test_pool().await;
|
||||||
|
let dialog_storage = SqliteStorage::new(pool.clone(), Json).await.unwrap();
|
||||||
|
let app = App::new(Box::new(mock_bot), DAOs::new(pool));
|
||||||
|
let me_user = create_tele_user("me");
|
||||||
|
let me = Me {
|
||||||
|
user: me_user,
|
||||||
|
can_join_groups: true,
|
||||||
|
can_read_all_group_messages: true,
|
||||||
|
supports_inline_queries: true,
|
||||||
|
can_connect_to_business: true,
|
||||||
|
has_main_web_app: true,
|
||||||
|
};
|
||||||
|
dptree::deps![update, dialog_storage, app, me]
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
|
|||||||
Reference in New Issue
Block a user