From 7cbe1e70e440c29375425182e661ea9ff7bd6300 Mon Sep 17 00:00:00 2001 From: Max Audron Date: Sat, 16 Oct 2021 13:49:23 +0200 Subject: rework configuration to allow extensibility by hooks --- .gitignore | 2 +- Cargo.lock | 81 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 ++-- config.debug.toml | 5 +++ config.toml | 17 ++++++---- src/config.rs | 80 ++++++++++++++++++++++++++++++++++++++++----- src/hooks/mod.rs | 2 +- src/hooks/wolfram_alpha.rs | 38 +++++++++++++++------- src/lib.rs | 35 ++++++-------------- src/main.rs | 4 ++- 10 files changed, 213 insertions(+), 58 deletions(-) create mode 100644 config.debug.toml diff --git a/.gitignore b/.gitignore index 05e354e..9b6afdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target /.cache/ -config.json /deploy/vendor +/config.debug.toml diff --git a/Cargo.lock b/Cargo.lock index 914a422..70e4c42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3410529e8288c463bedb5930f82833bc0c90e5d2fe639a56582a4d09220b281" +dependencies = [ + "autocfg", +] + [[package]] name = "atty" version = "0.2.14" @@ -130,6 +139,7 @@ dependencies = [ "async-trait", "base64", "catinator_macros", + "figment", "futures", "irc", "irc-proto", @@ -317,6 +327,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "figment" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790b4292c72618abbab50f787a477014fe15634f96291de45672ce46afe122df" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", +] + [[package]] name = "flate2" version = "1.0.22" @@ -596,6 +620,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inlinable_string" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3094308123a0e9fd59659ce45e22de9f53fc1d2ac6e1feb9fef988e4f76cad77" + [[package]] name = "instant" version = "0.1.9" @@ -854,6 +884,29 @@ dependencies = [ "crypto-mac", ] +[[package]] +name = "pear" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e44241c5e4c868e3eaa78b7c1848cadd6344ed4f54d029832d32b415a58702" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a5ca643c2303ecb740d506539deba189e16f2754040a42901cd8105d0282d0" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -919,6 +972,19 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + [[package]] name = "quote" version = "1.0.9" @@ -1479,6 +1545,15 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +[[package]] +name = "uncased" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.7" @@ -1680,3 +1755,9 @@ checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ "winapi", ] + +[[package]] +name = "yansi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" diff --git a/Cargo.toml b/Cargo.toml index 6d2f5e3..e0f9d2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,14 +12,15 @@ irc-proto = "0.15" sasl = "0.5" base64 = "0.13" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +figment = { version = "0.10", features = ["env", "toml"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" toml = "0.5" anyhow = "1" futures = "0.3" -tokio = { version = "1.5.0", features = ["full", "rt-multi-thread"] } +tokio = { version = "1", features = ["full", "rt-multi-thread"] } tracing = "0.1" tracing-subscriber = "0.2" diff --git a/config.debug.toml b/config.debug.toml new file mode 100644 index 0000000..9e7a0e7 --- /dev/null +++ b/config.debug.toml @@ -0,0 +1,5 @@ +[debug] +[debug.user] +nickname = "kittynator" +[debug.server] +channels = ["#audron-test"] diff --git a/config.toml b/config.toml index 4ce6caf..fba4301 100644 --- a/config.toml +++ b/config.toml @@ -1,16 +1,19 @@ -[user] -nickname = "\\__{^-_-^}" +[default] +[default.user] username = "catinator" -password = "" realname = "moaw" -[server] +[default.server] hostname = "irc.snoonet.org" port = 6697 tls = true sasl = true -channels = ["#redoxmasterrace", "#linuxmasterrace", "#gnulag"] -[settings] +[default.settings] prefix = ':' -wa_api_key = "" + +[release] +[release.user] +nickname = "\\__{^-_-^}" +[release.server] +channels = ["#redoxmasterrace", "#linuxmasterrace", "#gnulag"] diff --git a/src/config.rs b/src/config.rs index 776d60c..1a88b95 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,12 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize)] +use figment::{ + providers::{Format, Toml}, + value::{Dict, Map}, + Error, Figment, Metadata, Profile, Provider, +}; + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] pub struct Config { pub user: User, pub server: Server, @@ -13,7 +19,7 @@ impl From for irc::client::prelude::Config { nickname: Some(input.user.nickname), username: Some(input.user.username), realname: Some(input.user.realname), - nick_password: Some(input.user.password), + nick_password: input.user.password, server: Some(input.server.hostname), port: Some(input.server.port), use_tls: Some(input.server.tls), @@ -23,25 +29,83 @@ impl From for irc::client::prelude::Config { } } -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize)] +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] pub struct User { pub nickname: String, pub username: String, - pub password: String, + #[serde(default)] + pub password: Option, pub realname: String, } -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize)] +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] pub struct Server { pub hostname: String, + #[serde(default = "default_port")] pub port: u16, + #[serde(default = "default_tls")] pub tls: bool, + #[serde(default)] pub sasl: bool, + #[serde(default)] pub channels: Vec, } -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize)] +const fn default_port() -> u16 { + 6697 +} + +const fn default_tls() -> bool { + true +} + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] pub struct Settings { + #[serde(default = "default_prefix")] pub prefix: char, - pub wa_api_key: String, + // pub wa_api_key: String, +} + +const fn default_prefix() -> char { + ':' +} + +impl Config { + // Allow the configuration to be extracted from any `Provider`. + pub fn from(provider: T) -> Result { + Figment::from(provider).extract() + } + + // Provide a default provider, a `Figment`. + pub fn figment() -> Figment { + use figment::providers::Env; + + let figment = Figment::new(); + + #[cfg(debug_assertions)] + const PROFILE: &str = "debug"; + #[cfg(not(debug_assertions))] + const PROFILE: &str = "release"; + + figment + .merge(Toml::file("config.toml").nested()) + .merge(Toml::file("config.debug.toml").nested()) + .merge(Env::prefixed("CATINATOR_").split('_')) + .select(PROFILE) + } +} + +// Make `Config` a provider itself for composability. +impl Provider for Config { + fn metadata(&self) -> Metadata { + Metadata::named("Library Config") + } + + fn data(&self) -> Result, Error> { + figment::providers::Serialized::defaults(self).data() + } + + fn profile(&self) -> Option { + Some(Profile::Default) + } } diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index cdf3787..86d5697 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -36,7 +36,7 @@ pub fn sasl(bot: &crate::Bot, msg: Message) -> Result<()> { if text == "+" { let creds = Credentials::default() .with_username(bot.config.clone().user.username) - .with_password(bot.config.clone().user.password); + .with_password(bot.config.clone().user.password.unwrap()); let mut mechanism = Plain::from_credentials(creds)?; diff --git a/src/hooks/wolfram_alpha.rs b/src/hooks/wolfram_alpha.rs index 3faddbf..0fceb0b 100644 --- a/src/hooks/wolfram_alpha.rs +++ b/src/hooks/wolfram_alpha.rs @@ -3,12 +3,39 @@ use crate::util::{ web::{quote_plus, IsgdUrlShortener, UrlShortener}, }; use anyhow::{bail, Context, Error, Result}; +use figment::providers::Env; use futures::join; use irc::client::prelude::*; use macros::privmsg; use reqwest::{get, Url}; use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WolframAlpha { + wa_api_key: String, +} + +impl WolframAlpha { + pub fn new(bot: &crate::Bot) -> Result { + bot.figment + .clone() + .merge(Env::prefixed("CATINATOR_")) + .extract() + .context("failed to extract wolfram alpha config") + } + + pub async fn wa(&self, bot: &crate::Bot, msg: Message) -> Result<()> { + privmsg!(msg, { + let content = get_input_query(text)?; + bot.send_privmsg( + msg.response_target() + .context("failed to get response target")?, + &wa_query(&content, Some(&self.wa_api_key), None).await?, + )?; + }) + } +} + #[derive(Serialize, Deserialize, Debug)] struct WaResponse { queryresult: QueryResult, @@ -147,17 +174,6 @@ fn get_input_query(text: &str) -> Result { Ok(content.to_string()) } -pub async fn wa(bot: &crate::Bot, msg: Message) -> Result<()> { - privmsg!(msg, { - let content = get_input_query(text)?; - bot.send_privmsg( - msg.response_target() - .context("failed to get response target")?, - &wa_query(&content, Some(&bot.config.settings.wa_api_key), None).await?, - )?; - }) -} - #[cfg(test)] mod tests { diff --git a/src/lib.rs b/src/lib.rs index 82bdad5..37e9d89 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,12 +2,10 @@ #[cfg(all(test, feature = "bench"))] extern crate test; -use anyhow::Result; +use anyhow::{Context, Result}; use irc::client::prelude::*; -use tracing::info; - pub mod config; pub mod hooks; pub mod util; @@ -23,44 +21,29 @@ macro_rules! reply { pub struct Bot { pub config: config::Config, + pub figment: figment::Figment, pub irc_client: irc::client::Client, } -fn get_env_var(var_name: &str) -> Option { - match std::env::var(var_name) { - Ok(var) => { - info!("using {} from env", var_name); - Some(var) - } - Err(_) => None, - } -} - impl Bot { - pub async fn new(config_path: &str) -> Result { - use std::fs; + pub async fn new() -> Result { + let figment = config::Config::figment(); + let config: config::Config = figment.extract().context("failed to extract config")?; + + let irc_client = Client::from_config(config.clone().into()).await?; - let config_str = fs::read_to_string(config_path)?; - let mut config: config::Config = toml::from_str(&config_str)?; let bot = Bot { irc_client, config, figment }; - if let Some(v) = get_env_var("CATINATOR_PASSWORD") { - config.user.password = v - }; if bot.config.server.sasl && bot.config.user.password.is_some() { tracing::info!("initializing sasl"); bot.sasl_init().await.unwrap() } - if let Some(v) = get_env_var("CATINATOR_WA_API_KEY") { - config.settings.wa_api_key = v - }; - - let irc_client = Client::from_config(config.clone().into()).await?; Ok(bot) } - Ok(Bot { irc_client, config }) + pub fn figment(&self) -> &figment::Figment { + &self.figment } pub async fn sasl_init(&self) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 100af91..2428dff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,8 @@ async fn main() { let mut bot = Bot::new().await.unwrap(); let mut sed = catinator::hooks::sed::Sed::new(); + let wolfram_alpha = catinator::hooks::wolfram_alpha::WolframAlpha::new(&bot) + .expect("failed to initialize WolframAlpha command"); catinator![ hook( @@ -57,7 +59,7 @@ async fn main() { async command( "wa", "Returns Wolfram Alpha results for a query", - catinator::hooks::wolfram_alpha::wa + wolfram_alpha.wa ), ]; } -- cgit v1.2.3