diff --git a/echoircd.conf.example b/echoircd.conf.example index e4422ca..b2d439e 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -120,6 +120,8 @@ amu_target = both # --- chanlog (m_chanlog): mirror the oper server-notice stream into a channel so # staff can watch it in a normal window. Set the channel (create/keep it opped): # chanlog = #snotices +# --- helpmode (m_helpmode): no config — adds oper-settable user mode +h (helpop), +# which shows "is available for help" in the user's WHOIS. # --- globops (m_globops): no config — adds the oper command /GLOBOPS , # broadcasting to all opers (like the server-notice stream). # --- autodrop (m_autodrop): silently drop a not-yet-registered client that sends diff --git a/src/coremods/core_info.rs b/src/coremods/core_info.rs index ffb734a..480aaa9 100644 --- a/src/coremods/core_info.rs +++ b/src/coremods/core_info.rs @@ -253,6 +253,10 @@ impl Command for Whois { if let Some(line) = crate::modules::profilelink::line(s, &account) { s.numeric(uid, RPL_WHOISSPECIAL, &format!(":{line}")); } + // helpmode: +h marks a user available for help (visible to everyone) + if s.users.get(&tuid).map(|u| u.flags.helpop).unwrap_or(false) { + s.numeric(uid, RPL_WHOISSPECIAL, ":is available for help."); + } // customtitle: a claimed vanity title if let Some(line) = crate::modules::customtitle::line(s, tuid) { s.numeric(uid, RPL_WHOISSPECIAL, &format!(":{line}")); diff --git a/src/mode.rs b/src/mode.rs index 78bdb35..14c01d5 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -1064,6 +1064,7 @@ pub fn user_mode(c: char) -> Option<&'static (dyn UserMode + Sync)> { static USER_MODES: &[&(dyn UserMode + Sync)] = &[ &INVISIBLE, &WALLOPS, + &HELPOP, &OPER, &CLOAK, &BOT, @@ -1196,6 +1197,27 @@ impl UserMode for OperMode { } } +/// `+h` — helpop: marks a user as available for help (shown in WHOIS). Only opers +/// may set it on themselves; anyone may clear it. +struct HelpMode; +static HELPOP: HelpMode = HelpMode; +impl UserMode for HelpMode { + fn letter(&self) -> char { + 'h' + } + fn apply(&self, s: &mut Server, uid: Uid, adding: bool) -> bool { + if adding && !s.is_oper(uid) { + return false; // only opers may declare themselves a helpop + } + if let Some(u) = s.users.get_mut(&uid) { + u.flags.helpop = adding; + true + } else { + false + } + } +} + /// `+x` — host cloaking. The cloak string is computed once at connect by /// [`crate::modules::cloak`]; this handler only toggles whether it's shown. /// `-x` (revealing the real host) is oper-only, so +x can't be flipped to dodge @@ -1301,7 +1323,7 @@ mod tests { #[test] fn registry_covers_all_user_modes() { - for c in "iwoxBDIHrRzsgWc".chars() { + for c in "iwoxBDIHrRzsgWhc".chars() { assert!(user_mode(c).is_some(), "missing umode +{c}"); } assert!(user_mode('Q').is_none()); diff --git a/src/users.rs b/src/users.rs index 5f19f29..b4ced01 100644 --- a/src/users.rs +++ b/src/users.rs @@ -30,6 +30,7 @@ pub struct UserFlags { pub snomask: bool, // +s (oper: receive server notices) pub callerid: bool, // +g (only accept PMs from users on the ACCEPT list) pub showwhois: bool, // +W (get a notice when someone WHOISes you) + pub helpop: bool, // +h (helpop: available for help; shown in WHOIS) pub deny_uncommon: bool, // +c (only users sharing a channel may PM you) pub nick_locked: bool, // NICKLOCK: services/oper holds this nick (no self-change) pub via_webirc: bool, // connected through a WEBIRC gateway (securitygroups) @@ -82,6 +83,9 @@ impl UserFlags { if self.showwhois { s.push('W'); } + if self.helpop { + s.push('h'); + } if self.deny_uncommon { s.push('c'); } @@ -465,7 +469,7 @@ impl Server { uid, RPL_MYINFO, &format!( - "{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJUdKXwD", + "{} echoircd-{VERSION} iowxsgBDIHrRzWhc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJUdKXwD", self.name ), );