219 lines
6.7 KiB
Rust
219 lines
6.7 KiB
Rust
mod bidding;
|
|
mod bot_result;
|
|
mod commands;
|
|
mod config;
|
|
mod db;
|
|
mod dptree_utils;
|
|
mod handle_error;
|
|
mod handler_utils;
|
|
mod keyboard_utils;
|
|
mod message_sender;
|
|
mod message_utils;
|
|
mod sqlite_storage;
|
|
mod start_command_data;
|
|
#[cfg(test)]
|
|
mod test_utils;
|
|
mod wrap_endpoint;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use crate::bidding::{bidding_handler, BiddingState};
|
|
use crate::commands::{
|
|
my_listings::{my_listings_handler, my_listings_inline_handler, MyListingsState},
|
|
new_listing::{new_listing_handler, NewListingState},
|
|
};
|
|
use crate::db::DAOs;
|
|
use crate::handle_error::with_error_handler;
|
|
use crate::handler_utils::{find_or_create_db_user_from_update, update_into_message_target};
|
|
use crate::message_sender::{BotMessageSender, BoxedMessageSender};
|
|
use crate::sqlite_storage::SqliteStorage;
|
|
use anyhow::Result;
|
|
pub use bot_result::*;
|
|
use commands::*;
|
|
use config::Config;
|
|
use log::info;
|
|
use serde::{Deserialize, Serialize};
|
|
use teloxide::dispatching::dialogue::serializer::Json;
|
|
use teloxide::{prelude::*, types::BotCommand, utils::command::BotCommands};
|
|
pub use wrap_endpoint::*;
|
|
|
|
#[derive(Clone)]
|
|
pub struct App {
|
|
pub bot: Arc<BoxedMessageSender>,
|
|
pub daos: DAOs,
|
|
}
|
|
|
|
impl App {
|
|
pub fn new(bot: BoxedMessageSender, daos: DAOs) -> Self {
|
|
Self {
|
|
bot: Arc::new(bot),
|
|
daos,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Set up the bot's command menu that appears when users tap the menu button
|
|
async fn setup_bot_commands(bot: &Bot) -> Result<()> {
|
|
info!("Setting up bot command menu...");
|
|
|
|
// Convert our Command enum to Telegram BotCommand structs
|
|
let commands: Vec<BotCommand> = Command::bot_commands()
|
|
.into_iter()
|
|
.map(|cmd| BotCommand::new(cmd.command, cmd.description))
|
|
.collect();
|
|
|
|
// Set the commands for the bot's menu
|
|
bot.set_my_commands(commands).await?;
|
|
info!("Bot command menu configured successfully");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(BotCommands, Clone)]
|
|
#[command(rename_rule = "lowercase", description = "Auction Bot Commands")]
|
|
pub enum Command {
|
|
#[command(description = "Show welcome message")]
|
|
Start,
|
|
#[command(description = "Show help message")]
|
|
Help,
|
|
#[command(description = "Create a new listing or auction")]
|
|
NewListing,
|
|
#[command(description = "View your listings and auctions")]
|
|
MyListings,
|
|
#[command(description = "View your active bids")]
|
|
MyBids,
|
|
#[command(description = "Configure notifications")]
|
|
Settings,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
enum DialogueRootState {
|
|
#[default]
|
|
Start,
|
|
MainMenu,
|
|
NewListing(NewListingState),
|
|
MyListings(MyListingsState),
|
|
Bidding(BiddingState),
|
|
}
|
|
|
|
type RootDialogue = Dialogue<DialogueRootState, SqliteStorage<Json>>;
|
|
|
|
pub fn main_handler() -> BotHandler {
|
|
dptree::entry()
|
|
.map(|daos: DAOs| daos.user.clone())
|
|
.map(|daos: DAOs| daos.listing.clone())
|
|
.map(|daos: DAOs| daos.bid.clone())
|
|
.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]
|
|
async fn main() -> Result<()> {
|
|
// Load and validate configuration from environment/.env file
|
|
let config = Config::from_env()?;
|
|
|
|
// Create database connection pool
|
|
let db_pool = config.create_database_pool().await?;
|
|
|
|
info!("Starting Pawctioneer Bot...");
|
|
let bot = Box::new(Bot::new(&config.telegram_token));
|
|
|
|
// Set up the bot's command menu
|
|
setup_bot_commands(&bot).await?;
|
|
|
|
let handler_with_deps = dptree::entry()
|
|
.filter_map(|bot: Box<Bot>, update: Update, daos: DAOs| {
|
|
let target = update_into_message_target(update)?;
|
|
Some(App::new(
|
|
Box::new(BotMessageSender::new(*bot, target)),
|
|
daos.clone(),
|
|
))
|
|
})
|
|
.chain(main_handler());
|
|
|
|
let dialog_storage = SqliteStorage::new(db_pool.clone(), Json).await?;
|
|
let daos = DAOs::new(db_pool.clone());
|
|
|
|
Dispatcher::builder(bot, handler_with_deps)
|
|
.dependencies(dptree::deps![dialog_storage, daos])
|
|
.enable_ctrlc_handler()
|
|
.worker_queue_size(1)
|
|
.build()
|
|
.dispatch()
|
|
.await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn unknown_message_handler(msg: Message) -> BotResult {
|
|
Err(BotError::UserVisibleError(format!(
|
|
"Unknown command: `{}`\n\n\
|
|
Try /help to see the list of commands.\
|
|
",
|
|
msg.text().unwrap_or("")
|
|
)))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
|
|
use mockall::predicate::{always, function};
|
|
|
|
use super::*;
|
|
use crate::message_sender::MockMessageSender;
|
|
use crate::test_utils::{create_deps, create_tele_update};
|
|
|
|
#[tokio::test]
|
|
async fn test_main_handler() {
|
|
let mut message_sender = MockMessageSender::new();
|
|
message_sender
|
|
.expect_send_html_message()
|
|
.times(1)
|
|
.with(
|
|
function(|text: &String| text.contains("Available Commands:")),
|
|
always(),
|
|
)
|
|
.returning(|_, _| Ok(()));
|
|
let mut deps = create_deps(message_sender).await;
|
|
deps.insert(create_tele_update("/help"));
|
|
let handler = main_handler();
|
|
dptree::type_check(handler.sig(), &deps, &[]);
|
|
|
|
let result = handler.dispatch(deps).await;
|
|
assert!(matches!(result, ControlFlow::Break(Ok(()))), "{:?}", result);
|
|
}
|
|
}
|