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.
This commit is contained in:
projectmoon 2020-09-28 21:35:05 +00:00 committed by ProjectMoon
parent a5ec5c1e12
commit 938107feae
8 changed files with 670 additions and 225 deletions

351
Cargo.lock generated
View File

@ -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"

View File

@ -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"]

View File

@ -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<P: Into<PathBuf>>(config_path: P) -> Result<Config, Box<dyn std::error::Error>> {
fn read_config<P: Into<PathBuf>>(config_path: P) -> Result<Config, BotError> {
let config_path = config_path.into();
let config = {
let contents = fs::read_to_string(&config_path)?;
@ -14,13 +20,20 @@ fn read_config<P: Into<PathBuf>>(config_path: P) -> Result<Config, Box<dyn std::
Ok(config)
}
fn deserialize_config(contents: &str) -> Result<Config, Box<dyn std::error::Error>> {
let config = { toml::from_str(&contents)? };
fn deserialize_config(contents: &str) -> Result<Config, BotError> {
let config = toml::from_str(&contents)?;
Ok(config)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[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<dyn std::error::Error>> {
.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(())
}

View File

@ -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<u64>,
}
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<BotConfig>,
}
/// 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<Mutex<DiceBotState>>,
/// Current state of the dice bot. Actor ref to the core state
/// actor.
state: Addr<DiceBotState>,
}
fn cache_dir() -> Result<PathBuf, BotError> {
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<Client, BotError> {
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<DiceBotState>) -> Result<Self, BotError> {
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<dyn std::error::Error>> {
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));
}
}

110
src/config.rs Normal file
View File

@ -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<u64>,
}
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<BotConfig>,
}
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());
}
}

33
src/error.rs Normal file
View File

@ -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),
}

View File

@ -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;

56
src/state.rs Normal file
View File

@ -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<Self>;
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<LogSkippedOldMessages> for DiceBotState {
type Result = bool;
fn handle(&mut self, _msg: LogSkippedOldMessages, _ctx: &mut Context<Self>) -> Self::Result {
self.skipped_old_messages();
self.logged_skipped_old_messages
}
}