From 938107feae074546c052e1b29c2e7079e5bb79ff Mon Sep 17 00:00:00 2001 From: projectmoon Date: Mon, 28 Sep 2020 21:35:05 +0000 Subject: [PATCH] Implement Actix for state, refactor bot code. Instead of using an Arc Mutex for state management embedded directly into the bot, utilize actor pattern, with the idea that this will be much more useful than simply logging a message once in the future. This also refactors the bot code so that instead of a single run_bot function, the DiceBot struct now has a run() method attached to it. This also necessitated changes and cleanup to the dicebot main, which is for the better anyhow. The error and config types are also now in their own files, and implemented for more in-depth use cases. --- Cargo.lock | 351 ++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 +- src/bin/dicebot.rs | 35 +++-- src/bot.rs | 303 ++++++++++++-------------------------- src/config.rs | 110 ++++++++++++++ src/error.rs | 33 +++++ src/lib.rs | 3 + src/state.rs | 56 ++++++++ 8 files changed, 670 insertions(+), 225 deletions(-) create mode 100644 src/config.rs create mode 100644 src/error.rs create mode 100644 src/state.rs 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 + } +}