customprefix: add ranktoset/ranktounset/depriv per tier (InspIRCd change= parity for existing prefixes)
This commit is contained in:
parent
749a1c3b69
commit
9f3081cb38
2 changed files with 102 additions and 26 deletions
21
src/mode.rs
21
src/mode.rs
|
|
@ -158,10 +158,25 @@ impl ChanMode for Prefix {
|
||||||
return Applied::No;
|
return Applied::No;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// must out-rank (or match) both the prefix being set and the target's
|
// customprefix depriv=no: a member may not remove this prefix from themselves
|
||||||
// current top rank — no de-opping someone above you.
|
if !adding && tuid == uid && !crate::modules::customprefix::can_depriv(self.ch) {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_CHANOPRIVSNEEDED,
|
||||||
|
&format!("{chan} :You may not remove +{} from yourself", self.ch),
|
||||||
|
);
|
||||||
|
return Applied::No;
|
||||||
|
}
|
||||||
|
// must out-rank (or match) both the rank needed to set/unset this prefix
|
||||||
|
// (customprefix ranktoset/ranktounset, default the prefix's own rank) and the
|
||||||
|
// target's current top rank — no de-opping someone above you.
|
||||||
|
let needed = if adding {
|
||||||
|
crate::modules::customprefix::rank_to_set(self.ch).unwrap_or(self.rank)
|
||||||
|
} else {
|
||||||
|
crate::modules::customprefix::rank_to_unset(self.ch).unwrap_or(self.rank)
|
||||||
|
};
|
||||||
let src = s.rank(uid, key);
|
let src = s.rank(uid, key);
|
||||||
if src < self.rank || src < target_rank {
|
if src < needed || src < target_rank {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
ERR_CHANOPRIVSNEEDED,
|
ERR_CHANOPRIVSNEEDED,
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
//! customprefix — override the *symbol* (sigil) of the channel prefix tiers from
|
//! customprefix — reconfigure the channel prefix tiers from config, like InspIRCd's
|
||||||
//! config, e.g. show `!` for op instead of `@`. One line per tier:
|
//! m_customprefix does for existing prefixes (`change="yes"`). One line per tier:
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! customprefix = op * # ops show as *nick, PREFIX advertises it too
|
//! customprefix = op * ranktoset=admin ranktounset=admin depriv=no
|
||||||
//! customprefix = voice -
|
//! customprefix = voice -
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Tiers: `oper founder admin op halfop voice`. Only the displayed symbol changes —
|
//! Tiers: `oper founder admin op halfop voice`. Knobs:
|
||||||
//! the mode letters (`yqaohv`) and ranks stay fixed, so NAMES/WHO, ISUPPORT PREFIX
|
//! * a bare token = the displayed sigil (e.g. `*`)
|
||||||
//! and the S2S FJOIN burst stay consistent (a linked network must share this config,
|
//! * `ranktoset=<rank>` min rank to grant this prefix (default: the prefix's rank)
|
||||||
//! as with InspIRCd). Loaded once at boot; unset tiers keep their default sigil.
|
//! * `ranktounset=<rank>` min rank to revoke it (default: ranktoset)
|
||||||
|
//! * `depriv=no` members may not remove this prefix from themselves
|
||||||
|
//!
|
||||||
|
//! A `<rank>` is a number (1–6) or a tier name (`op`, `admin`, …). Only the display
|
||||||
|
//! and set/unset policy change — 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). Adding brand-new tiers with new letters/ranks
|
||||||
|
//! would need a data-driven prefix engine and isn't supported. Loaded once at boot.
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
|
@ -18,37 +25,68 @@ use crate::server::Server;
|
||||||
const TIER_NAMES: [&str; 6] = ["oper", "founder", "admin", "op", "halfop", "voice"];
|
const TIER_NAMES: [&str; 6] = ["oper", "founder", "admin", "op", "halfop", "voice"];
|
||||||
const LETTERS: [char; 6] = ['y', 'q', 'a', 'o', 'h', 'v'];
|
const LETTERS: [char; 6] = ['y', 'q', 'a', 'o', 'h', 'v'];
|
||||||
const DEFAULT_SIGILS: [&str; 6] = ["!", "~", "&", "@", "%", "+"];
|
const DEFAULT_SIGILS: [&str; 6] = ["!", "~", "&", "@", "%", "+"];
|
||||||
|
const RANKS: [u8; 6] = [6, 5, 4, 3, 2, 1]; // oper..voice
|
||||||
|
|
||||||
/// The configured sigils, indexed by tier. Being a `static` its `String`s live for
|
struct PrefixCfg {
|
||||||
/// the program, so `sigil()` can hand out `&'static str` without leaking.
|
sigils: [String; 6],
|
||||||
static SIGILS: OnceLock<[String; 6]> = OnceLock::new();
|
ranktoset: [Option<u8>; 6],
|
||||||
|
ranktounset: [Option<u8>; 6],
|
||||||
|
depriv: [bool; 6],
|
||||||
|
}
|
||||||
|
|
||||||
|
static CFG: OnceLock<PrefixCfg> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Parse a rank: a number (1–6) or a tier name.
|
||||||
|
fn parse_rank(v: &str) -> Option<u8> {
|
||||||
|
if let Ok(n) = v.parse::<u8>() {
|
||||||
|
return Some(n.min(6));
|
||||||
|
}
|
||||||
|
TIER_NAMES
|
||||||
|
.iter()
|
||||||
|
.position(|t| t.eq_ignore_ascii_case(v))
|
||||||
|
.map(|i| RANKS[i])
|
||||||
|
}
|
||||||
|
|
||||||
/// Load the `customprefix` overrides once, at boot.
|
/// Load the `customprefix` overrides once, at boot.
|
||||||
pub fn init(s: &Server) {
|
pub fn init(s: &Server) {
|
||||||
let mut a: [String; 6] = DEFAULT_SIGILS.map(String::from);
|
let mut cfg = PrefixCfg {
|
||||||
|
sigils: DEFAULT_SIGILS.map(String::from),
|
||||||
|
ranktoset: [None; 6],
|
||||||
|
ranktounset: [None; 6],
|
||||||
|
depriv: [true; 6],
|
||||||
|
};
|
||||||
for line in s.conf_all("customprefix") {
|
for line in s.conf_all("customprefix") {
|
||||||
let mut it = line.split_whitespace();
|
let mut it = line.split_whitespace();
|
||||||
if let (Some(name), Some(sym)) = (it.next(), it.next()) {
|
let Some(name) = it.next() else { continue };
|
||||||
if let Some(i) = TIER_NAMES.iter().position(|t| t.eq_ignore_ascii_case(name)) {
|
let Some(i) = TIER_NAMES.iter().position(|t| t.eq_ignore_ascii_case(name)) else {
|
||||||
if let Some(c) = sym.chars().next() {
|
continue;
|
||||||
a[i] = c.to_string();
|
};
|
||||||
|
for tok in it {
|
||||||
|
match tok.split_once('=') {
|
||||||
|
Some(("ranktoset", v)) => cfg.ranktoset[i] = parse_rank(v),
|
||||||
|
Some(("ranktounset", v)) => cfg.ranktounset[i] = parse_rank(v),
|
||||||
|
Some(("depriv", v)) => cfg.depriv[i] = crate::config::yesish(v),
|
||||||
|
Some(_) => {}
|
||||||
|
None => {
|
||||||
|
// a bare token is the sigil
|
||||||
|
if let Some(c) = tok.chars().next() {
|
||||||
|
cfg.sigils[i] = c.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = SIGILS.set(a);
|
}
|
||||||
|
let _ = CFG.set(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The sigil for tier `i` (0 = oper … 5 = voice).
|
/// The sigil for tier `i` (0 = oper … 5 = voice).
|
||||||
pub fn sigil(i: usize) -> &'static str {
|
pub fn sigil(i: usize) -> &'static str {
|
||||||
SIGILS
|
CFG.get()
|
||||||
.get()
|
.map(|c| c.sigils[i].as_str())
|
||||||
.map(|a| a[i].as_str())
|
|
||||||
.unwrap_or(DEFAULT_SIGILS[i])
|
.unwrap_or(DEFAULT_SIGILS[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The prefix mode letter for a sigil char (used by the FJOIN decode); `' '` if the
|
/// The prefix mode letter for a sigil char (FJOIN decode); `' '` if none.
|
||||||
/// char isn't a prefix sigil.
|
|
||||||
pub fn letter_for_sigil(c: char) -> char {
|
pub fn letter_for_sigil(c: char) -> char {
|
||||||
let cs = c.to_string();
|
let cs = c.to_string();
|
||||||
(0..6)
|
(0..6)
|
||||||
|
|
@ -57,8 +95,31 @@ pub fn letter_for_sigil(c: char) -> char {
|
||||||
.unwrap_or(' ')
|
.unwrap_or(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ISUPPORT `PREFIX=(modes)symbols` token; `include_oper` adds the `y` tier
|
fn index_for_letter(letter: char) -> Option<usize> {
|
||||||
/// (operprefix/ojoin).
|
LETTERS.iter().position(|&l| l == letter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured minimum rank to grant the prefix with mode letter `letter` (None ⇒
|
||||||
|
/// use the prefix's own rank).
|
||||||
|
pub fn rank_to_set(letter: char) -> Option<u8> {
|
||||||
|
let i = index_for_letter(letter)?;
|
||||||
|
CFG.get()?.ranktoset[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured minimum rank to revoke the prefix (None ⇒ use the prefix's own rank).
|
||||||
|
pub fn rank_to_unset(letter: char) -> Option<u8> {
|
||||||
|
let i = index_for_letter(letter)?;
|
||||||
|
CFG.get()?.ranktounset[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a member may remove this prefix from themselves (default yes).
|
||||||
|
pub fn can_depriv(letter: char) -> bool {
|
||||||
|
index_for_letter(letter)
|
||||||
|
.and_then(|i| CFG.get().map(|c| c.depriv[i]))
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ISUPPORT `PREFIX=(modes)symbols` token; `include_oper` adds the `y` tier.
|
||||||
pub fn isupport(include_oper: bool) -> String {
|
pub fn isupport(include_oper: bool) -> String {
|
||||||
let start = if include_oper { 0 } else { 1 };
|
let start = if include_oper { 0 } else { 1 };
|
||||||
let letters: String = LETTERS[start..].iter().collect();
|
let letters: String = LETTERS[start..].iter().collect();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue