use serde::{self, Deserialize, Serialize}; use std::env; use std::fs; use std::path::PathBuf; use thiserror::Error; #[derive(Error, Debug)] pub enum ConfigError { #[error("i/o error: {0}")] IoError(#[from] std::io::Error), #[error("toml parsing error: {0}")] TomlParsingError(#[from] toml::de::Error), } pub fn read_config>(config_path: P) -> Result { let config_path = config_path.into(); let config = { let contents = fs::read_to_string(&config_path)?; deserialize_config(&contents)? }; Ok(config) } fn deserialize_config(contents: &str) -> Result { let config = toml::from_str(&contents)?; Ok(config) } /// The "matrix" section of the config, which gives home server, login information, and etc. #[derive(Serialize, Deserialize, Clone, Debug)] struct MatrixConfig { /// Your homeserver of choice, as an FQDN without scheme or path home_server: String, /// Username to login as. Only the localpart. username: String, /// Bot account password. password: String, } const DEFAULT_OLDEST_MESSAGE_AGE: u64 = 15 * 60; fn db_path_from_env() -> String { env::var("DATABASE_PATH") .expect("could not find database path in config or environment variable") } /// The "bot" section of the config file, for bot settings. #[derive(Serialize, Deserialize, Clone, Debug, Default)] struct BotConfig { /// How far back from current time should we process a message? oldest_message_age: Option, /// What address and port to run the RPC service on. If not /// specified, RPC will not be enabled. rpc_addr: Option, /// The shared secret key between the bot and any RPC clients that /// want to connect to it. The RPC server will reject any clients /// that don't present the shared key. rpc_key: Option, } /// The "database" section of the config file. #[derive(Serialize, Deserialize, Clone, Debug)] struct DatabaseConfig { /// Path to the database storage directory. Required. path: Option, } impl DatabaseConfig { #[inline] #[must_use] fn path(&self) -> String { self.path.clone().unwrap_or_else(|| db_path_from_env()) } } impl BotConfig { /// 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). #[inline] #[must_use] fn oldest_message_age(&self) -> u64 { self.oldest_message_age .unwrap_or(DEFAULT_OLDEST_MESSAGE_AGE) } #[inline] #[must_use] fn rpc_addr(&self) -> Option { self.rpc_addr.clone() } #[inline] #[must_use] fn rpc_key(&self) -> Option { self.rpc_key.clone() } } /// Represents the toml config file for the dicebot. The sections of /// the config are not directly accessible; instead the config /// provides friendly methods that handle default values, etc. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Config { matrix: MatrixConfig, database: Option, bot: Option, } impl Config { /// The matrix homeserver URL. #[inline] #[must_use] pub fn matrix_homeserver(&self) -> &str { &self.matrix.home_server } /// The username used to connect to the matrix server. #[inline] #[must_use] pub fn matrix_username(&self) -> &str { &self.matrix.username } /// The password used to connect to the matrix server. #[inline] #[must_use] pub fn matrix_password(&self) -> &str { &self.matrix.password } /// The path to the database storage directory. #[inline] #[must_use] pub fn database_path(&self) -> String { self.database .as_ref() .map(|db| db.path()) .unwrap_or_else(|| db_path_from_env()) } /// 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 default to 15 minutes. #[inline] #[must_use] pub fn oldest_message_age(&self) -> u64 { self.bot .as_ref() .map(|bc| bc.oldest_message_age()) .unwrap_or(DEFAULT_OLDEST_MESSAGE_AGE) } #[inline] #[must_use] pub fn rpc_addr(&self) -> Option { self.bot.as_ref().and_then(|bc| bc.rpc_addr()) } #[inline] #[must_use] pub fn rpc_key(&self) -> Option { self.bot.as_ref().and_then(|bc| bc.rpc_key()) } } #[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(), }, database: Some(DatabaseConfig { path: Some("".to_owned()), }), bot: Some(BotConfig { oldest_message_age: None, ..Default::default() }), }; assert_eq!(15 * 60, cfg.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(), }, database: Some(DatabaseConfig { path: Some("".to_owned()), }), bot: None, }; assert_eq!(15 * 60, cfg.oldest_message_age()); } #[test] fn db_path_uses_setting_first_test() { let cfg = Config { matrix: MatrixConfig { home_server: "".to_owned(), username: "".to_owned(), password: "".to_owned(), }, database: Some(DatabaseConfig { path: Some("the-db-path".to_owned()), }), bot: None, }; assert_eq!("the-db-path".to_owned(), cfg.database_path()); } #[test] fn db_path_uses_env_if_setting_not_defined_test() { env::set_var("DATABASE_PATH", "the-db-path"); let cfg = Config { matrix: MatrixConfig { home_server: "".to_owned(), username: "".to_owned(), password: "".to_owned(), }, database: Some(DatabaseConfig { path: None }), bot: None, }; assert_eq!("the-db-path".to_owned(), cfg.database_path()); env::remove_var("DATABASE_PATH"); } #[test] fn db_path_uses_env_if_section_not_defined_test() { env::set_var("DATABASE_PATH", "the-db-path"); let cfg = Config { matrix: MatrixConfig { home_server: "".to_owned(), username: "".to_owned(), password: "".to_owned(), }, database: None, bot: None, }; assert_eq!("the-db-path".to_owned(), cfg.database_path()); env::remove_var("DATABASE_PATH"); } use indoc::indoc; #[test] fn deserialize_config_without_bot_section_test() { let contents = indoc! {" [matrix] home_server = 'https://matrix.example.com' username = 'username' password = 'password' [database] path = '' "}; let cfg: Result<_, _> = deserialize_config(contents); assert_eq!(true, cfg.is_ok()); } #[test] fn deserialize_config_without_oldest_message_setting_test() { let contents = indoc! {" [matrix] home_server = 'https://matrix.example.com' username = 'username' password = 'password' [database] path = '' [bot] not_a_real_setting = 2 "}; let cfg: Result<_, _> = deserialize_config(contents); assert_eq!(true, cfg.is_ok()); } #[test] fn deserialize_config_without_db_path_setting_test() { let contents = indoc! {" [matrix] home_server = 'https://matrix.example.com' username = 'username' password = 'password' [database] not_a_real_setting = 1 [bot] not_a_real_setting = 2 "}; let cfg: Result<_, _> = deserialize_config(contents); assert_eq!(true, cfg.is_ok()); } #[test] fn deserialize_config_without_db_section_test() { let contents = indoc! {" [matrix] home_server = 'https://matrix.example.com' username = 'username' password = 'password' [bot] not_a_real_setting = 2 "}; let cfg: Result<_, _> = deserialize_config(contents); assert_eq!(true, cfg.is_ok()); } }