diff --git a/README.md b/README.md index f68c64e..2cb57d3 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ lists them at runtime and `help ` describes one. |---|---| | `ping` / `echo ` / `hello` | basics | | `roll [NdM]` | roll dice (default `1d6`) | -| `karma [thing]` / `karmabottom` (bump with `thing++ [reason]` / `thing--`) | karma + rank + last reason, or top/bottom; milestones announced | +| `karma [thing]` / `karmabottom` / `karmaweb` (bump with `thing++ [reason]` / `thing--`) | karma + rank + last reason, or top/bottom (with a link to the web board when `karma_url` is set); `karmaweb` prints that link; milestones announced | | `md5` / `sha1` / `sha256` / `hash ` | hex digests | | `add ` then `w [location]` | weather (wttr.in), per-user location | | `crypto ` | coin price in USD (Bitstamp) | @@ -124,7 +124,9 @@ RUSTBOT_CONFIG=/etc/rustbot.conf cargo run Config file keys: `server`, `port`, `tls`, `nick`, `user`, `realname`, `channels` (comma/space-separated), `prefix`, `verbose` (wire logging, default on), `password`, `sasl_user`, `sasl_pass`, `name` (a network's log label), and -`locale` (`en`/`es`/`fr`, default `en`). Whole-line `#`/`;` comments only — +`locale` (`en`/`es`/`fr`, default `en`), and `karma_url` (public web board — +enables the `karmaweb` command and a link on the `karma`/`karmabottom` top +lists; empty disables both). Whole-line `#`/`;` comments only — inline comments would clash with channel `#`s. ### Locales diff --git a/src/bot/mod.rs b/src/bot/mod.rs index c308558..522cce5 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -28,7 +28,11 @@ impl Bot { pub fn new(config: Config, conn: Connection) -> Bot { let current_nick = config.nick.clone(); - let mods = modules::all(&config.name, crate::i18n::Locale::from_code(&config.locale)); + let mods = modules::all( + &config.name, + crate::i18n::Locale::from_code(&config.locale), + (!config.karma_url.is_empty()).then(|| config.karma_url.clone()), + ); let mut routes: HashMap<&'static str, usize> = HashMap::new(); for (i, m) in mods.iter().enumerate() { for spec in m.commands() { diff --git a/src/bot/modules/karma.rs b/src/bot/modules/karma.rs index 2fb4737..d516ec0 100644 --- a/src/bot/modules/karma.rs +++ b/src/bot/modules/karma.rs @@ -20,6 +20,11 @@ const COMMANDS: &[CommandSpec] = &[ usage: "karmabottom", about: "the lowest-karma things", }, + CommandSpec { + name: "karmaweb", + usage: "karmaweb", + about: "link to the web karma board", + }, ]; const MAX_THING_LEN: usize = 32; @@ -31,10 +36,11 @@ const ZWSP: char = '\u{200B}'; pub struct Karma { store: Store, locale: Locale, + url: Option, } impl Karma { - pub fn new(network: &str, locale: Locale) -> Karma { + pub fn new(network: &str, locale: Locale, url: Option) -> Karma { let dir = std::env::var("RUSTBOT_DATA_DIR").unwrap_or_else(|_| ".".to_string()); let safe: String = network .chars() @@ -44,6 +50,7 @@ impl Karma { PathBuf::from(dir).join(format!("karma.{safe}.json")), locale, ) + .with_url(url) } pub fn with_path(path: PathBuf) -> Karma { @@ -54,9 +61,15 @@ impl Karma { Karma { store: Store::load(path), locale, + url: None, } } + pub fn with_url(mut self, url: Option) -> Karma { + self.url = url; + self + } + pub fn get(&self, thing: &str) -> i64 { self.store.entry(thing).map(|e| e.count).unwrap_or(0) } @@ -112,7 +125,31 @@ impl Karma { (Locale::Fr, false) => "top karma", (Locale::Fr, true) => "pire karma", }; - format!("{label}: {}", items.join(", ")) + let mut out = format!("{label}: {}", items.join(", ")); + if let Some(u) = &self.url { + let more = match self.locale { + Locale::En => "full board", + Locale::Es => "tablero completo", + Locale::Fr => "tableau complet", + }; + out.push_str(&format!(" · {more}: {u}")); + } + out + } + + fn web_link(&self) -> String { + match &self.url { + Some(u) => match self.locale { + Locale::En => format!("karma board: {u}"), + Locale::Es => format!("tablero de karma: {u}"), + Locale::Fr => format!("tableau de karma : {u}"), + }, + None => match self.locale { + Locale::En => "no web board configured".to_string(), + Locale::Es => "no hay tablero web configurado".to_string(), + Locale::Fr => "pas de tableau web configuré".to_string(), + }, + } } } @@ -132,6 +169,7 @@ impl Module for Karma { None => self.leaderboard(false), }, "karmabottom" => self.leaderboard(true), + "karmaweb" => self.web_link(), _ => return vec![], }; vec![Action::reply(reply)] diff --git a/src/bot/modules/mod.rs b/src/bot/modules/mod.rs index ea6ba53..79ef314 100644 --- a/src/bot/modules/mod.rs +++ b/src/bot/modules/mod.rs @@ -13,12 +13,12 @@ pub mod wiki; use super::module::Module; use crate::i18n::Locale; -pub fn all(network: &str, locale: Locale) -> Vec> { +pub fn all(network: &str, locale: Locale, karma_url: Option) -> Vec> { vec![ Box::new(builtins::Builtins), Box::new(dice::Dice::new()), Box::new(hash::Hash), - Box::new(karma::Karma::new(network, locale)), + Box::new(karma::Karma::new(network, locale, karma_url)), Box::new(crypto::Crypto), Box::new(define::Define), Box::new(down::Down), diff --git a/src/config.rs b/src/config.rs index 6bd3d73..6056028 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,6 +19,7 @@ pub struct Config { pub sasl_user: Option, pub sasl_pass: Option, pub locale: String, + pub karma_url: String, } impl Default for Config { @@ -38,6 +39,7 @@ impl Default for Config { sasl_user: None, sasl_pass: None, locale: "en".to_string(), + karma_url: String::new(), } } } @@ -163,6 +165,7 @@ fn apply_kv(c: &mut Config, key: &str, value: &str, where_: &str) { "sasl_user" => c.sasl_user = (!value.is_empty()).then(|| value.to_string()), "sasl_pass" => c.sasl_pass = (!value.is_empty()).then(|| value.to_string()), "locale" | "lang" => c.locale = value.to_string(), + "karma_url" | "web_url" => c.karma_url = value.to_string(), other => log(&format!("{where_}: unknown key '{other}'")), } } @@ -223,6 +226,9 @@ fn apply_env(c: &mut Config) { if let Ok(v) = std::env::var("IRC_LOCALE") { c.locale = v; } + if let Ok(v) = std::env::var("IRC_KARMA_URL") { + c.karma_url = v; + } } fn parse_bool(value: &str) -> bool { diff --git a/tests/karma.rs b/tests/karma.rs index 159b1ff..44c5a6c 100644 --- a/tests/karma.rs +++ b/tests/karma.rs @@ -259,3 +259,38 @@ fn replies_in_spanish_for_es_locale() { let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&log); } + +#[test] +fn karmaweb_command_and_leaderboard_suffix() { + let path = std::env::temp_dir().join("rubot-test-karma-web.json"); + let log = std::env::temp_dir().join("rubot-test-karma-web.events.jsonl"); + let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(&log); + let mut k = + Karma::with_path(path.clone()).with_url(Some("https://karma.devtronic.pro/".to_string())); + // dedicated command returns the link + let w = reply_of(&mut k, "karmaweb", &[]); + assert!(w.contains("https://karma.devtronic.pro/"), "{w}"); + // link is appended to the aggregate list... + k.on_message(&chat("a", "rust++ python++")); + let top = reply_of(&mut k, "karma", &[]); + assert!(top.starts_with("karma top:"), "{top}"); + assert!( + top.contains("full board: https://karma.devtronic.pro/"), + "{top}" + ); + // ...but not to a single-thing lookup (kept clean) + let one = reply_of(&mut k, "karma", &["rust"]); + assert!(!one.contains("http"), "{one}"); + let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(&log); +} + +#[test] +fn karmaweb_without_url_is_graceful() { + let path = std::env::temp_dir().join("rubot-test-karma-nourl.json"); + let _ = std::fs::remove_file(&path); + let mut k = Karma::with_path(path.clone()); + assert!(reply_of(&mut k, "karmaweb", &[]).contains("no web board configured")); + let _ = std::fs::remove_file(&path); +}