diff --git a/Cargo.lock b/Cargo.lock index c222300..e6b568e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,96 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "actix" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be241f88f3b1e7e9a3fbe3b5a8a0f6915b5a1d7ee0d9a248d3376d01068cc60" +dependencies = [ + "actix-rt", + "actix_derive", + "bitflags", + "bytes", + "crossbeam-channel", + "derive_more", + "futures-channel", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project", + "smallvec", + "tokio", + "tokio-util", + "trust-dns-proto", + "trust-dns-resolver", +] + +[[package]] +name = "actix-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a60f9ba7c4e6df97f3aacb14bb5c0cd7d98a49dcbaed0d7f292912ad9a6a3ed2" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-rt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227" +dependencies = [ + "actix-macros", + "actix-threadpool", + "copyless", + "futures-channel", + "futures-util", + "smallvec", + "tokio", +] + +[[package]] +name = "actix-threadpool" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d209f04d002854b9afd3743032a27b066158817965bf5d036824d19ac2cc0e30" +dependencies = [ + "derive_more", + "futures-channel", + "lazy_static", + "log", + "num_cpus", + "parking_lot", + "threadpool", +] + +[[package]] +name = "actix_derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95aceadaf327f18f0df5962fedc1bde2f870566a0b9f65c89508a3b1f79334c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "addr2line" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + [[package]] name = "ahash" version = "0.3.8" @@ -112,6 +203,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1931848a574faa8f7c71a12ea00453ff5effbb5f51afe7f77d7a48cace6ac1" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.12.3" @@ -169,6 +274,8 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" name = "chronicle-dicebot" version = "0.6.0" dependencies = [ + "actix", + "actix-rt", "async-trait", "dirs", "env_logger", @@ -200,6 +307,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "cloudabi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" +dependencies = [ + "bitflags", +] + [[package]] name = "cmake" version = "0.1.44" @@ -235,6 +351,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "copyless" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" + [[package]] name = "core-foundation" version = "0.7.0" @@ -251,6 +373,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" +[[package]] +name = "crossbeam-channel" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +dependencies = [ + "crossbeam-utils", + "maybe-uninit", +] + [[package]] name = "crossbeam-queue" version = "0.2.3" @@ -284,6 +416,17 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "derive_more" +version = "0.99.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dirs" version = "3.0.1" @@ -331,6 +474,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-as-inner" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -542,6 +697,12 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" + [[package]] name = "gloo-timers" version = "0.2.1" @@ -604,6 +765,17 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi 0.3.9", +] + [[package]] name = "http" version = "0.2.1" @@ -735,6 +907,18 @@ dependencies = [ "libc", ] +[[package]] +name = "ipconfig" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" +dependencies = [ + "socket2", + "widestring", + "winapi 0.3.9", + "winreg 0.6.2", +] + [[package]] name = "ipnet" version = "2.3.0" @@ -820,6 +1004,21 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + +[[package]] +name = "lock_api" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.11" @@ -829,12 +1028,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matches" version = "0.1.8" @@ -981,6 +1195,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60c0dfe32c10b43a144bad8fc83538c52f58302c92300ea7ec7bf7b38d5a7b9" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.6.22" @@ -1073,6 +1297,12 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" + [[package]] name = "olm-rs" version = "1.0.0" @@ -1133,6 +1363,32 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +dependencies = [ + "cfg-if", + "cloudabi", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1345,7 +1601,17 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.7.0", +] + +[[package]] +name = "resolv-conf" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11834e137f3b14e309437a8276714eed3a80d1ef894869e510f2c0c0b98b9f4a" +dependencies = [ + "hostname", + "quick-error", ] [[package]] @@ -1607,6 +1873,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rustc-demangle" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" + [[package]] name = "ryu" version = "1.0.5" @@ -1623,6 +1895,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "security-framework" version = "0.4.4" @@ -1711,6 +1989,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +[[package]] +name = "smallvec" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" + [[package]] name = "socket2" version = "0.3.15" @@ -1911,6 +2195,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "time" version = "0.1.44" @@ -1980,6 +2273,7 @@ checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "log", "pin-project-lite", @@ -2043,6 +2337,46 @@ dependencies = [ "tracing", ] +[[package]] +name = "trust-dns-proto" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd7061ba6f4d4d9721afedffbfd403f20f39a4301fee1b70d6fcd09cca69f28" +dependencies = [ + "async-trait", + "backtrace", + "enum-as-inner", + "futures 0.3.5", + "idna", + "lazy_static", + "log", + "rand", + "smallvec", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f23cdfdc3d8300b3c50c9e84302d3bd6d860fb9529af84ace6cf9665f181b77" +dependencies = [ + "backtrace", + "cfg-if", + "futures 0.3.5", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -2226,6 +2560,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "widestring" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a763e303c0e0f23b0da40888724762e802a8ffefbc22de4127ef42493c2ea68c" + [[package]] name = "winapi" version = "0.2.8" @@ -2269,6 +2609,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winreg" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index b0f80ed..b566bfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ async-trait = "0.1" url = "2.1" dirs = "3.0" indoc = "1.0" +actix = "0.10" +actix-rt = "1.1" # The versioning of the matrix SDK follows its Cargo.toml. The SDK and # macros are on master, but it imports the common and base from 0.1.0. @@ -37,4 +39,4 @@ features = ['derive'] [dependencies.tokio] version = "0.2" -features = ["rt-core", "macros", "time", "signal"] +features = ["rt-core", "rt-util", "macros", "time", "signal"] diff --git a/src/bin/dicebot.rs b/src/bin/dicebot.rs index 651291d..19927ea 100644 --- a/src/bin/dicebot.rs +++ b/src/bin/dicebot.rs @@ -1,10 +1,16 @@ -use chronicle_dicebot::bot::run_bot; -use chronicle_dicebot::bot::Config; +//Needed for nested Result handling from tokio. Probably can go away after 1.47.0. +#![type_length_limit = "7605144"] +use actix::prelude::*; +use chronicle_dicebot::bot::DiceBot; +use chronicle_dicebot::config::*; +use chronicle_dicebot::error::BotError; +use chronicle_dicebot::state::DiceBotState; use env_logger::Env; +use log::error; use std::fs; use std::path::PathBuf; -fn read_config>(config_path: P) -> Result> { +fn read_config>(config_path: P) -> Result { let config_path = config_path.into(); let config = { let contents = fs::read_to_string(&config_path)?; @@ -14,13 +20,20 @@ fn read_config>(config_path: P) -> Result Result> { - let config = { toml::from_str(&contents)? }; +fn deserialize_config(contents: &str) -> Result { + let config = toml::from_str(&contents)?; Ok(config) } -#[tokio::main] -async fn main() -> Result<(), Box> { +#[actix_rt::main] +async fn main() { + match run().await { + Ok(_) => (), + Err(e) => error!("Error: {:?}", e), + }; +} + +async fn run() -> Result<(), BotError> { env_logger::from_env(Env::default().default_filter_or("chronicle_dicebot=info")).init(); let config_path = std::env::args() @@ -29,8 +42,14 @@ async fn main() -> Result<(), Box> { .expect("Need a config as an argument"); let cfg = read_config(config_path)?; + let bot_state = DiceBotState::new(&cfg).start(); - run_bot(cfg).await?; + match DiceBot::new(&cfg, bot_state) { + Ok(bot) => bot.run().await?, + Err(e) => println!("Error connecting: {:?}", e), + }; + + System::current().stop(); Ok(()) } diff --git a/src/bot.rs b/src/bot.rs index 4b3fa41..1f8d2a2 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,4 +1,8 @@ use crate::commands::parse_command; +use crate::config::*; +use crate::error::BotError; +use crate::state::{DiceBotState, LogSkippedOldMessages}; +use actix::Addr; use dirs; use log::{debug, error, info, trace, warn}; use matrix_sdk::{ @@ -11,135 +15,102 @@ use matrix_sdk::{ Client, ClientConfig, EventEmitter, JsonStore, SyncRoom, SyncSettings, }; use matrix_sdk_common_macros::async_trait; -use serde::{self, Deserialize, Serialize}; use std::clone::Clone; use std::ops::Sub; -use std::sync::{Arc, Mutex}; +use std::path::PathBuf; use std::time::{Duration, SystemTime}; -use thiserror::Error; use url::Url; -//TODO move the config structs and read_config into their own file. - -/// The "matrix" section of the config, which gives home server, login information, and etc. -#[derive(Serialize, Deserialize, Debug)] -pub struct MatrixConfig { - /// Your homeserver of choice, as an FQDN without scheme or path - pub home_server: String, - - /// Username to login as. Only the localpart. - pub username: String, - - /// Bot account password. - pub password: String, -} - -const DEFAULT_OLDEST_MESSAGE_AGE: u64 = 15 * 60; - -/// The "bot" section of the config file, for bot settings. -#[derive(Serialize, Deserialize, Debug)] -pub struct BotConfig { - /// How far back from current time should we process a message? - oldest_message_age: Option, -} - -impl BotConfig { - pub fn new() -> BotConfig { - BotConfig { - oldest_message_age: Some(DEFAULT_OLDEST_MESSAGE_AGE), - } - } - - /// Determine the oldest allowable message age, in seconds. If the - /// setting is defined, use that value. If it is not defined, fall - /// back to DEFAULT_OLDEST_MESSAGE_AGE (15 minutes). - pub fn oldest_message_age(&self) -> u64 { - match self.oldest_message_age { - Some(seconds) => seconds, - None => DEFAULT_OLDEST_MESSAGE_AGE, - } - } -} - -/// Represents the toml config file for the dicebot. -#[derive(Serialize, Deserialize, Debug)] -pub struct Config { - pub matrix: MatrixConfig, - pub bot: Option, -} - -/// The DiceBot struct itself is the core of the program, essentially the entrypoint -/// to the bot. +/// The DiceBot struct represents an active dice bot. The bot is not +/// connected to Matrix until its run() function is called. pub struct DiceBot { /// A reference to the configuration read in on application start. config: Config, - /// The matrix SDK client. + /// The matrix client. client: Client, - /// Current state of the dice bot. Held in an Arc because it - /// accessed by the multi-threaded matrix SDK event handlers. - state: Arc>, + /// Current state of the dice bot. Actor ref to the core state + /// actor. + state: Addr, +} + +fn cache_dir() -> Result { + let mut dir = dirs::cache_dir().ok_or(BotError::NoCacheDirectoryError)?; + dir.push("matrix-dicebot"); + Ok(dir) +} + +/// Creates the matrix client. +fn create_client(config: &Config) -> Result { + let cache_dir = cache_dir()?; + let store = JsonStore::open(&cache_dir)?; + let client_config = ClientConfig::new().state_store(Box::new(store)); + let homeserver_url = Url::parse(&config.matrix.home_server)?; + + Ok(Client::new_with_config(homeserver_url, client_config)?) } impl DiceBot { - /// Create a new dicebot with the given Matrix configuration and - /// client. The dice bot is iniitalized with a fresh state. - pub fn new(config: Config, client: Client) -> Self { - DiceBot { - config: config, - client: client, - state: Arc::new(Mutex::new(DiceBotState::new())), - } - } -} - -/// Holds state of the dice bot, for anything requiring mutable -/// transitions. This is a simple mutable trait whose values represent -/// the current state of the dicebot. It provides mutable methods to -/// change state. -//#[derive(Clone, Copy)] -pub struct DiceBotState { - logged_skipped_old_message: bool, -} - -impl DiceBotState { - /// Create initial dice bot state. - fn new() -> DiceBotState { - DiceBotState { - logged_skipped_old_message: false, - } + /// 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: &Config, state_actor: Addr) -> Result { + Ok(DiceBot { + client: create_client(&config)?, + config: config.clone(), + state: state_actor, + }) } - /// Log and record that we have skipped some old messages. This - /// method will log once, and then no-op from that point on. - fn skipped_old_messages(&mut self) { - if !self.logged_skipped_old_message { - info!("Skipped some messages received while offline because they are too old."); + /// Logs the bot into 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 username = &self.config.matrix.username; + let password = &self.config.matrix.password; + + //TODO provide a device id from config. + let mut client = self.client.clone(); + client + .login(username, password, None, Some("matrix dice bot")) + .await?; + + info!("Logged in as {}", username); + + //If the local json store has not been created yet, we need to do a single initial sync. + //It stores data under username's localpart. + let should_sync = { + let username = &self.config.matrix.username; + let mut cache = cache_dir()?; + cache.push(username); + !cache.exists() + }; + + if should_sync { + info!("Performing initial sync"); + self.client.sync(SyncSettings::default()).await?; } - self.logged_skipped_old_message = true; + //Attach event handler. + client.add_event_emitter(Box::new(self)).await; + info!("Listening for commands"); + + let token = client + .sync_token() + .await + .ok_or(BotError::SyncTokenRequired)?; + + let settings = SyncSettings::default().token(token); + + //this keeps state from the server streaming in to the dice bot via the EventEmitter trait + //TODO somehow figure out how to "sync_until" instead of sync_forever... copy code and modify? + client.sync_forever(settings, |_| async {}).await; + Ok(()) } } -/// Figure out the allowed oldest message age, in seconds. This will -/// be the defined oldest message age in the bot config, if the bot -/// configuration and associated "oldest_message_age" setting are -/// defined. If the bot config or the message setting are not defined, -/// it will defualt to 15 minutes. -fn get_oldest_message_age(config: &Config) -> u64 { - let none_cfg; - let bot_cfg = match &config.bot { - Some(cfg) => cfg, - None => { - none_cfg = BotConfig::new(); - &none_cfg - } - }; - - bot_cfg.oldest_message_age() -} - /// Check if a message is recent enough to actually process. If the /// message is within "oldest_message_age" seconds, this function /// returns true. If it's older than that, it returns false and logs a @@ -215,14 +186,16 @@ impl EventEmitter for DiceBot { } //Ignore messages that are older than configured duration. - if !check_message_age(event, get_oldest_message_age(&self.config)) { - let mut state = match self.state.lock() { - Ok(state) => state, - Err(poisoned) => poisoned.into_inner(), - }; + if !check_message_age(event, self.config.get_oldest_message_age()) { + let res = self.state.send(LogSkippedOldMessages).await; - (*state).skipped_old_messages(); - return; + match res { + Ok(_) => return, + Err(e) => { + error!("Actix error: {:?}", e); + return; + } + } } let (plain, html) = match parse_command(&msg_body) { @@ -257,103 +230,3 @@ impl EventEmitter for DiceBot { } } } - -#[derive(Error, Debug)] -pub enum BotError { - /// Sync token couldn't be found. - #[error("the sync token could not be retrieved")] - SyncTokenRequired, -} - -/// Run the matrix dice bot until program terminated, or a panic occurs. -/// Originally adapted from the matrix-rust-sdk command bot example. -pub async fn run_bot(config: Config) -> Result<(), Box> { - let homeserver_url = &config.matrix.home_server; - let username = &config.matrix.username; - let password = &config.matrix.password; - - let mut cache_dir = dirs::cache_dir().expect("no cache directory found"); - cache_dir.push("matrix-dicebot"); - - //If the local json store has not been created yet, we need to do a single initial sync. - //It stores data under username's localpart. - let should_sync = { - let mut cache = cache_dir.clone(); - cache.push(username.clone()); - !cache.exists() - }; - - let store = JsonStore::open(&cache_dir)?; - let client_config = ClientConfig::new().state_store(Box::new(store)); - - let homeserver_url = Url::parse(homeserver_url).expect("Couldn't parse the homeserver URL"); - let mut client = Client::new_with_config(homeserver_url, client_config).unwrap(); - - client - .login(username, password, None, Some("matrix dice bot")) - .await?; - - info!("Logged in as {}", username); - - if should_sync { - info!("Performing initial sync"); - client.sync(SyncSettings::default()).await?; - } - - //Attach event handler. - info!("Listening for commands"); - info!( - "Oldest allowable message time is {} seconds ago", - get_oldest_message_age(&config) - ); - - client - .add_event_emitter(Box::new(DiceBot::new(config, client.clone()))) - .await; - - let token = client - .sync_token() - .await - .ok_or(BotError::SyncTokenRequired)?; - let settings = SyncSettings::default().token(token); - - //this keeps state from the server streaming in to the dice bot via the EventEmitter trait - client.sync_forever(settings, |_| async {}).await; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn oldest_message_default_no_setting_test() { - let cfg = Config { - matrix: MatrixConfig { - home_server: "".to_owned(), - username: "".to_owned(), - password: "".to_owned(), - }, - bot: Some(BotConfig { - oldest_message_age: None, - }), - }; - - assert_eq!(15 * 60, get_oldest_message_age(&cfg)); - } - - #[test] - fn oldest_message_default_no_bot_config_test() { - let cfg = Config { - matrix: MatrixConfig { - home_server: "".to_owned(), - username: "".to_owned(), - password: "".to_owned(), - }, - bot: None, - }; - - assert_eq!(15 * 60, get_oldest_message_age(&cfg)); - } -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..55e384d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,110 @@ +use serde::{self, Deserialize, Serialize}; + +/// The "matrix" section of the config, which gives home server, login information, and etc. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct MatrixConfig { + /// Your homeserver of choice, as an FQDN without scheme or path + pub home_server: String, + + /// Username to login as. Only the localpart. + pub username: String, + + /// Bot account password. + pub password: String, +} + +const DEFAULT_OLDEST_MESSAGE_AGE: u64 = 15 * 60; + +/// The "bot" section of the config file, for bot settings. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct BotConfig { + /// How far back from current time should we process a message? + oldest_message_age: Option, +} + +impl BotConfig { + pub fn new() -> BotConfig { + BotConfig { + oldest_message_age: Some(DEFAULT_OLDEST_MESSAGE_AGE), + } + } + + /// Determine the oldest allowable message age, in seconds. If the + /// setting is defined, use that value. If it is not defined, fall + /// back to DEFAULT_OLDEST_MESSAGE_AGE (15 minutes). + pub fn oldest_message_age(&self) -> u64 { + match self.oldest_message_age { + Some(seconds) => seconds, + None => DEFAULT_OLDEST_MESSAGE_AGE, + } + } +} + +/// Represents the toml config file for the dicebot. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Config { + pub matrix: MatrixConfig, + pub bot: Option, +} + +impl Config { + pub fn bot(self) -> BotConfig { + let none_cfg; + let bot_cfg = match self.bot { + Some(cfg) => cfg, + None => { + none_cfg = BotConfig::new(); + none_cfg + } + }; + + bot_cfg + } + + /// Figure out the allowed oldest message age, in seconds. This will + /// be the defined oldest message age in the bot config, if the bot + /// configuration and associated "oldest_message_age" setting are + /// defined. If the bot config or the message setting are not defined, + /// it will defualt to 15 minutes. + pub fn get_oldest_message_age(&self) -> u64 { + self.bot + .as_ref() + .unwrap_or(&BotConfig::new()) + .oldest_message_age() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn oldest_message_default_no_setting_test() { + let cfg = Config { + matrix: MatrixConfig { + home_server: "".to_owned(), + username: "".to_owned(), + password: "".to_owned(), + }, + bot: Some(BotConfig { + oldest_message_age: None, + }), + }; + + assert_eq!(15 * 60, cfg.get_oldest_message_age()); + } + + #[test] + fn oldest_message_default_no_bot_config_test() { + let cfg = Config { + matrix: MatrixConfig { + home_server: "".to_owned(), + username: "".to_owned(), + password: "".to_owned(), + }, + bot: None, + }; + + assert_eq!(15 * 60, cfg.get_oldest_message_age()); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..709a1fb --- /dev/null +++ b/src/error.rs @@ -0,0 +1,33 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum BotError { + /// Sync token couldn't be found. + #[error("the sync token could not be retrieved")] + SyncTokenRequired, + + #[error("no cache directory found")] + NoCacheDirectoryError, + + #[error("could not parse URL")] + UrlParseError(#[from] url::ParseError), + + #[error("uncategorized matrix SDK error")] + MatrixError(#[from] matrix_sdk::Error), + + #[error("uncategorized matrix SDK base error")] + MatrixBaseError(#[from] matrix_sdk::BaseError), + + #[error("future canceled")] + FutureCanceledError, + + #[error("tokio task join error")] + TokioTaskJoinError(#[from] tokio::task::JoinError), + + //de = deserialization + #[error("toml parsing error")] + TomlParsingError(#[from] toml::de::Error), + + #[error("i/o error")] + IoError(#[from] std::io::Error), +} diff --git a/src/lib.rs b/src/lib.rs index 1bb1f4c..503e5d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,10 @@ pub mod bot; pub mod cofd; pub mod commands; +pub mod config; pub mod dice; +pub mod error; mod help; mod parser; pub mod roll; +pub mod state; diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..36c0e6e --- /dev/null +++ b/src/state.rs @@ -0,0 +1,56 @@ +use crate::config::*; +use actix::prelude::*; +use log::info; + +#[derive(Message)] +#[rtype(result = "bool")] +pub struct LogSkippedOldMessages; + +/// Holds state of the dice bot, for anything requiring mutable +/// transitions. This is a simple mutable trait whose values represent +/// the current state of the dicebot. It provides mutable methods to +/// change state. +pub struct DiceBotState { + logged_skipped_old_messages: bool, + config: Config, +} + +impl Actor for DiceBotState { + type Context = Context; + + fn started(&mut self, _ctx: &mut Self::Context) { + info!( + "Oldest allowable message time is {} seconds ago", + &self.config.get_oldest_message_age() + ); + } +} + +impl DiceBotState { + /// Create initial dice bot state. + pub fn new(config: &Config) -> DiceBotState { + DiceBotState { + logged_skipped_old_messages: false, + config: config.clone(), + } + } + + /// Log and record that we have skipped some old messages. This + /// method will log once, and then no-op from that point on. + pub fn skipped_old_messages(&mut self) { + if !self.logged_skipped_old_messages { + info!("Skipped some messages received while offline because they are too old."); + } + + self.logged_skipped_old_messages = true; + } +} + +impl Handler for DiceBotState { + type Result = bool; + + fn handle(&mut self, _msg: LogSkippedOldMessages, _ctx: &mut Context) -> Self::Result { + self.skipped_old_messages(); + self.logged_skipped_old_messages + } +}