use crate::commands::ExecutionResult; use crate::config::*; use crate::db::sqlite::Database; use crate::db::DbState; use crate::error::BotError; use crate::state::DiceBotState; use log::info; use matrix_sdk::room::Room; use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; use matrix_sdk::ruma::events::SyncMessageLikeEvent; use matrix_sdk::ruma::OwnedEventId; use matrix_sdk::{self, room::Joined, Client}; use matrix_sdk::config::SyncSettings; use std::clone::Clone; use std::sync::{Arc, RwLock}; mod command_execution; pub mod event_handlers; /// How many commands can be in one message. If the amount is higher /// than this, we reject execution. const MAX_COMMANDS_PER_MESSAGE: usize = 50; /// The DiceBot struct represents an active dice bot. The bot is not /// connected to Matrix until its run() function is called. #[derive(Clone)] pub struct DiceBot { /// A reference to the configuration read in on application start. config: Arc, /// The matrix client. client: Client, /// State of the dicebot state: Arc>, /// Active database layer db: Database, } impl DiceBot { /// Create a new dicebot with the given configuration and state /// actor. This function returns a Result because it is possible /// for client creation to fail for some reason (e.g. invalid /// homeserver URL). pub fn new( config: &Arc, state: &Arc>, db: &Database, client: &Client, ) -> Result { Ok(DiceBot { client: client.clone(), config: config.clone(), state: state.clone(), db: db.clone(), }) } /// Logs in to matrix and potentially records a new device ID. If /// no device ID is found in the database, a new one will be /// generated by the matrix SDK, and we will store it. async fn login(&self, client: &Client) -> Result<(), BotError> { let username = self.config.matrix_username(); let password = self.config.matrix_password(); // Pull device ID from database, if it exists. Then write it // to DB if the library generated one for us. let device_id: Option = self.db.get_device_id().await?; let device_id: Option<&str> = device_id.as_deref(); let no_device_ld_login = || client.login_username(username, password); let device_id_login = |id| client.login_username(username, password).device_id(id); let login = device_id.map_or_else(no_device_ld_login, device_id_login); login.send().await?; if device_id.is_none() { let device_id = client.device_id().ok_or(BotError::NoDeviceIdFound)?; self.db.set_device_id(device_id.as_str()).await?; info!("Recorded new device ID: {}", device_id.as_str()); } else { info!("Using existing device ID: {}", device_id.unwrap()); } info!("Logged in as {}", username); Ok(()) } async fn bind_events(&self) { //on room message: need closure to pass bot ref in. self.client .add_event_handler({ let bot: DiceBot = self.clone(); move |event: SyncMessageLikeEvent, room: Room| { let bot = bot.clone(); async move { event_handlers::on_room_message(event, room, bot).await } } }); //auto-join handler self.client .add_event_handler(event_handlers::on_stripped_state_member); } /// Logs the bot in to Matrix and listens for events until program /// terminated, or a panic occurs. Originally adapted from the /// matrix-rust-sdk command bot example. pub async fn run(self) -> Result<(), BotError> { let client = self.client.clone(); self.login(&client).await?; self.bind_events().await; info!("Listening for commands"); // TODO replace with sync_with_callback for cleaner shutdown // process. client.sync(SyncSettings::default()).await?; Ok(()) } async fn execute_commands( &self, room: &Joined, sender: &str, msg_body: &str, ) -> Vec<(String, ExecutionResult)> { let commands: Vec<&str> = msg_body .lines() .filter(|line| line.starts_with("!")) .take(MAX_COMMANDS_PER_MESSAGE + 1) .collect(); //Up to 50 commands allowed, otherwise we send back an error. let results: Vec<(String, ExecutionResult)> = if commands.len() < MAX_COMMANDS_PER_MESSAGE { command_execution::execute(commands, &self.db, &self.client, room, sender).await } else { vec![("".to_owned(), Err(BotError::MessageTooLarge))] }; results } pub async fn handle_results( &self, room: &Joined, sender_username: &str, event_id: OwnedEventId, results: Vec<(String, ExecutionResult)>, ) { if results.len() >= 1 { if results.len() == 1 { command_execution::handle_single_result( &self.client, &results[0].1, sender_username, &room, event_id, ) .await; } else if results.len() > 1 { command_execution::handle_multiple_results( &self.client, &results, sender_username, &room, ) .await; } } } }