diff --git a/src/channels.rs b/src/channels.rs index a671b80..5efe748 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -51,20 +51,22 @@ impl Member { } } - /// Highest prefix char for NAMES (`""` for a plain member). + /// Highest prefix char for NAMES (`""` for a plain member). The sigil per tier is + /// config-overridable via [`crate::modules::customprefix`]. pub fn prefix_char(&self) -> &'static str { + use crate::modules::customprefix::sigil; if self.oprefix { - "!" + sigil(0) } else if self.owner { - "~" + sigil(1) } else if self.admin { - "&" + sigil(2) } else if self.op { - "@" + sigil(3) } else if self.halfop { - "%" + sigil(4) } else if self.voice { - "+" + sigil(5) } else { "" } @@ -85,17 +87,18 @@ impl Member { /// Every prefix char this member holds, high→low (for the `multi-prefix` cap). pub fn all_prefixes(&self) -> String { + use crate::modules::customprefix::sigil; let mut s = String::new(); - for (on, c) in [ - (self.oprefix, '!'), - (self.owner, '~'), - (self.admin, '&'), - (self.op, '@'), - (self.halfop, '%'), - (self.voice, '+'), + for (on, i) in [ + (self.oprefix, 0), + (self.owner, 1), + (self.admin, 2), + (self.op, 3), + (self.halfop, 4), + (self.voice, 5), ] { if on { - s.push(c); + s.push_str(sigil(i)); } } s diff --git a/src/ircd.rs b/src/ircd.rs index 4995887..05c2a2b 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -103,6 +103,7 @@ impl Ircd { crate::modules::metadata::load(&mut server); // restore channel metadata crate::modules::reputation::load(&mut server); // restore per-IP reputation crate::modules::geoip::init(&mut server); // load the GeoIP database + crate::modules::customprefix::init(&server); // load prefix-sigil overrides Ircd { server, commands: command_table(), diff --git a/src/link.rs b/src/link.rs index 2ad2457..bc372ca 100644 --- a/src/link.rs +++ b/src/link.rs @@ -1434,14 +1434,8 @@ fn split_member(tok: &str) -> (String, String) { /// Map a prefix char to its mode letter (`@` → `o`, …). fn prefix_letter(c: char) -> char { - match c { - '~' => 'q', - '&' => 'a', - '@' => 'o', - '%' => 'h', - '+' => 'v', - _ => ' ', - } + // config-overridable sigils (customprefix); a linked network shares this config + crate::modules::customprefix::letter_for_sigil(c) } #[cfg(test)] diff --git a/src/modules/customprefix.rs b/src/modules/customprefix.rs new file mode 100644 index 0000000..928a9ae --- /dev/null +++ b/src/modules/customprefix.rs @@ -0,0 +1,67 @@ +//! customprefix — override the *symbol* (sigil) of the channel prefix tiers from +//! config, e.g. show `!` for op instead of `@`. One line per tier: +//! +//! ```text +//! customprefix = op * # ops show as *nick, PREFIX advertises it too +//! customprefix = voice - +//! ``` +//! +//! Tiers: `oper founder admin op halfop voice`. Only the displayed symbol changes — +//! the mode letters (`yqaohv`) and ranks stay fixed, so NAMES/WHO, ISUPPORT PREFIX +//! and the S2S FJOIN burst stay consistent (a linked network must share this config, +//! as with InspIRCd). Loaded once at boot; unset tiers keep their default sigil. + +use std::sync::OnceLock; + +use crate::server::Server; + +const TIER_NAMES: [&str; 6] = ["oper", "founder", "admin", "op", "halfop", "voice"]; +const LETTERS: [char; 6] = ['y', 'q', 'a', 'o', 'h', 'v']; +const DEFAULT_SIGILS: [&str; 6] = ["!", "~", "&", "@", "%", "+"]; + +/// The configured sigils, indexed by tier. Being a `static` its `String`s live for +/// the program, so `sigil()` can hand out `&'static str` without leaking. +static SIGILS: OnceLock<[String; 6]> = OnceLock::new(); + +/// Load the `customprefix` overrides once, at boot. +pub fn init(s: &Server) { + let mut a: [String; 6] = DEFAULT_SIGILS.map(String::from); + for line in s.conf_all("customprefix") { + let mut it = line.split_whitespace(); + if let (Some(name), Some(sym)) = (it.next(), it.next()) { + if let Some(i) = TIER_NAMES.iter().position(|t| t.eq_ignore_ascii_case(name)) { + if let Some(c) = sym.chars().next() { + a[i] = c.to_string(); + } + } + } + } + let _ = SIGILS.set(a); +} + +/// The sigil for tier `i` (0 = oper … 5 = voice). +pub fn sigil(i: usize) -> &'static str { + SIGILS + .get() + .map(|a| a[i].as_str()) + .unwrap_or(DEFAULT_SIGILS[i]) +} + +/// The prefix mode letter for a sigil char (used by the FJOIN decode); `' '` if the +/// char isn't a prefix sigil. +pub fn letter_for_sigil(c: char) -> char { + let cs = c.to_string(); + (0..6) + .find(|&i| sigil(i) == cs) + .map(|i| LETTERS[i]) + .unwrap_or(' ') +} + +/// The ISUPPORT `PREFIX=(modes)symbols` token; `include_oper` adds the `y` tier +/// (operprefix/ojoin). +pub fn isupport(include_oper: bool) -> String { + let start = if include_oper { 0 } else { 1 }; + let letters: String = LETTERS[start..].iter().collect(); + let sigils: String = (start..6).map(sigil).collect(); + format!("({letters}){sigils}") +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index a617353..dc3489d 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -20,6 +20,7 @@ pub mod conn_waitpong; pub mod connclass; pub mod connectban; pub mod connflood; +pub mod customprefix; pub mod customtitle; pub mod dccallow; pub mod denychans; diff --git a/src/server.rs b/src/server.rs index a0d0b93..b4874ce 100644 --- a/src/server.rs +++ b/src/server.rs @@ -616,12 +616,10 @@ impl Server { let chathist = crate::modules::chathistory::limit(self); let maxnick = self.conf_num("maxnick", 30usize); let maxchan = self.conf_num("maxchannel", 50usize); - // operprefix/ojoin add the server oper prefix `y` (sigil `!`) above owner - let prefix = if self.conf_bool("operprefix", false) || self.conf_bool("ojoin", false) { - "(yqaohv)!~&@%+" - } else { - "(qaohv)~&@%+" - }; + // operprefix/ojoin add the server oper prefix `y` above owner; sigils are + // config-overridable (see modules::customprefix) + let include_oper = self.conf_bool("operprefix", false) || self.conf_bool("ojoin", false); + let prefix = crate::modules::customprefix::isupport(include_oper); let mut lines = vec![format!( "CHANTYPES=# PREFIX={prefix} CHANMODES=beIgXw,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,Gbcgjmnrsy WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}", self.network