customprefix: data-driven prefix engine — define arbitrary new prefix modes (letter/prefix/rank/ranktoset/ranktounset/depriv), ranks re-spaced x10; built-in tiers + defaults unchanged
This commit is contained in:
parent
d35a2071a6
commit
b40c523b87
5 changed files with 256 additions and 102 deletions
|
|
@ -52,12 +52,16 @@ use_resolved_host = on
|
||||||
# allow a unique command prefix to resolve to its full command (e.g. WHOI -> WHOIS)
|
# allow a unique command prefix to resolve to its full command (e.g. WHOI -> WHOIS)
|
||||||
# abbreviation = yes
|
# abbreviation = yes
|
||||||
|
|
||||||
# reconfigure channel-prefix tiers (oper founder admin op halfop voice):
|
# customprefix — reconfigure built-in prefix tiers, or define brand-new ones.
|
||||||
# a bare token = the displayed sigil; ranktoset / ranktounset = min rank to grant /
|
# Built-in tiers (oper founder admin op halfop voice): a bare token = the sigil;
|
||||||
# revoke it (a number 1-6 or a tier name); depriv=no forbids removing it from
|
# ranktoset / ranktounset = min rank to grant / revoke it (a number or a tier name);
|
||||||
# yourself. (Symbols/policy only — adding brand-new tiers isn't supported.)
|
# depriv=no forbids removing it from yourself.
|
||||||
# customprefix = op * ranktoset=admin ranktounset=admin depriv=no
|
# customprefix = op * ranktoset=admin ranktounset=admin depriv=no
|
||||||
# customprefix = voice -
|
# customprefix = voice -
|
||||||
|
# New prefix (name is anything that isn't a built-in tier): letter + prefix required;
|
||||||
|
# rank (default 1), ranktoset/ranktounset (default rank), depriv (default yes).
|
||||||
|
# Ranks: voice=10 halfop=20 op=30 admin=40 founder=50 oper=60 (room to slot between).
|
||||||
|
# customprefix = helper letter=V prefix=? rank=25 ranktoset=op ranktounset=op
|
||||||
|
|
||||||
# DNS blocklist (DNSBL) checks on connect. Repeat `dnsbl`
|
# DNS blocklist (DNSBL) checks on connect. Repeat `dnsbl`
|
||||||
# for multiple zones. On a listing, `dnsbl_action` decides what happens:
|
# for multiple zones. On a listing, `dnsbl_action` decides what happens:
|
||||||
|
|
|
||||||
128
src/channels.rs
128
src/channels.rs
|
|
@ -18,22 +18,25 @@ pub struct Member {
|
||||||
pub op: bool, // +o (@)
|
pub op: bool, // +o (@)
|
||||||
pub halfop: bool, // +h (%)
|
pub halfop: bool, // +h (%)
|
||||||
pub voice: bool, // +v (+)
|
pub voice: bool, // +v (+)
|
||||||
|
pub custom_prefixes: Vec<char>, // config-defined prefix mode letters held (customprefix)
|
||||||
pub joined: u64, // unix ts this member joined (for +d delaymsg; 0 = unknown)
|
pub joined: u64, // unix ts this member joined (for +d delaymsg; 0 = unknown)
|
||||||
pub recent_msgs: Vec<String>, // +K repeat: this member's last few lines here
|
pub recent_msgs: Vec<String>, // +K repeat: this member's last few lines here
|
||||||
pub hidden: bool, // +D delayjoin: JOIN withheld until they reveal themselves
|
pub hidden: bool, // +D delayjoin: JOIN withheld until they reveal themselves
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prefix ranks, high→low — gate who may grant a prefix / kick whom.
|
/// Prefix ranks, high→low — gate who may grant a prefix / kick whom. Spaced ×10 so
|
||||||
pub const RANK_OPER: u8 = 6; // operprefix/ojoin — above channel owner (network staff)
|
/// config-defined custom prefixes (modules::customprefix) can slot in between.
|
||||||
pub const RANK_OWNER: u8 = 5;
|
pub const RANK_OPER: u8 = 60; // operprefix/ojoin — above channel owner (network staff)
|
||||||
pub const RANK_ADMIN: u8 = 4;
|
pub const RANK_OWNER: u8 = 50;
|
||||||
pub const RANK_OP: u8 = 3;
|
pub const RANK_ADMIN: u8 = 40;
|
||||||
pub const RANK_HALFOP: u8 = 2;
|
pub const RANK_OP: u8 = 30;
|
||||||
pub const RANK_VOICE: u8 = 1;
|
pub const RANK_HALFOP: u8 = 20;
|
||||||
|
pub const RANK_VOICE: u8 = 10;
|
||||||
|
|
||||||
impl Member {
|
impl Member {
|
||||||
/// This member's numeric rank (0 = plain member).
|
/// This member's numeric rank (0 = plain member).
|
||||||
pub fn rank(&self) -> u8 {
|
/// Built-in tier rank from the fixed booleans (0 = none), ignoring custom prefixes.
|
||||||
|
fn builtin_rank(&self) -> u8 {
|
||||||
if self.oprefix {
|
if self.oprefix {
|
||||||
RANK_OPER
|
RANK_OPER
|
||||||
} else if self.owner {
|
} else if self.owner {
|
||||||
|
|
@ -51,28 +54,70 @@ impl Member {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Highest prefix char for NAMES (`""` for a plain member). The sigil per tier is
|
pub fn rank(&self) -> u8 {
|
||||||
|
let mut r = self.builtin_rank();
|
||||||
|
for &c in &self.custom_prefixes {
|
||||||
|
if let Some(d) = crate::modules::customprefix::def_for_letter(c) {
|
||||||
|
r = r.max(d.rank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every (rank, sigil) prefix this member holds, high→low. Only allocates when
|
||||||
|
/// custom prefixes are actually present.
|
||||||
|
fn held(&self) -> Vec<(u8, &'static str)> {
|
||||||
|
use crate::modules::customprefix::{def_for_letter, sigil};
|
||||||
|
let mut v: Vec<(u8, &'static str)> = Vec::new();
|
||||||
|
for (on, r, i) in [
|
||||||
|
(self.oprefix, RANK_OPER, 0),
|
||||||
|
(self.owner, RANK_OWNER, 1),
|
||||||
|
(self.admin, RANK_ADMIN, 2),
|
||||||
|
(self.op, RANK_OP, 3),
|
||||||
|
(self.halfop, RANK_HALFOP, 4),
|
||||||
|
(self.voice, RANK_VOICE, 5),
|
||||||
|
] {
|
||||||
|
if on {
|
||||||
|
v.push((r, sigil(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for &c in &self.custom_prefixes {
|
||||||
|
if let Some(d) = def_for_letter(c) {
|
||||||
|
v.push((d.rank, d.sigil.as_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.sort_by(|a, b| b.0.cmp(&a.0));
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highest prefix char for NAMES (`""` for a plain member). Sigils are
|
||||||
/// config-overridable via [`crate::modules::customprefix`].
|
/// config-overridable via [`crate::modules::customprefix`].
|
||||||
pub fn prefix_char(&self) -> &'static str {
|
pub fn prefix_char(&self) -> &'static str {
|
||||||
use crate::modules::customprefix::sigil;
|
use crate::modules::customprefix::sigil;
|
||||||
if self.oprefix {
|
if self.custom_prefixes.is_empty() {
|
||||||
sigil(0)
|
// fast path: built-in tiers only
|
||||||
} else if self.owner {
|
if self.oprefix {
|
||||||
sigil(1)
|
sigil(0)
|
||||||
} else if self.admin {
|
} else if self.owner {
|
||||||
sigil(2)
|
sigil(1)
|
||||||
} else if self.op {
|
} else if self.admin {
|
||||||
sigil(3)
|
sigil(2)
|
||||||
} else if self.halfop {
|
} else if self.op {
|
||||||
sigil(4)
|
sigil(3)
|
||||||
} else if self.voice {
|
} else if self.halfop {
|
||||||
sigil(5)
|
sigil(4)
|
||||||
|
} else if self.voice {
|
||||||
|
sigil(5)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
""
|
self.held().first().map(|(_, s)| *s).unwrap_or("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set/clear a prefix mode by its letter (used by the S2S mode applier).
|
/// Set/clear a prefix mode by its letter — built-in booleans or, for a
|
||||||
|
/// config-defined letter, the custom-prefix set (used by the S2S mode applier).
|
||||||
pub fn set_prefix(&mut self, letter: char, on: bool) {
|
pub fn set_prefix(&mut self, letter: char, on: bool) {
|
||||||
match letter {
|
match letter {
|
||||||
'y' => self.oprefix = on,
|
'y' => self.oprefix = on,
|
||||||
|
|
@ -81,27 +126,38 @@ impl Member {
|
||||||
'o' => self.op = on,
|
'o' => self.op = on,
|
||||||
'h' => self.halfop = on,
|
'h' => self.halfop = on,
|
||||||
'v' => self.voice = on,
|
'v' => self.voice = on,
|
||||||
_ => {}
|
_ => {
|
||||||
|
if crate::modules::customprefix::def_for_letter(letter).is_some() {
|
||||||
|
self.custom_prefixes.retain(|&c| c != letter);
|
||||||
|
if on {
|
||||||
|
self.custom_prefixes.push(letter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every prefix char this member holds, high→low (for the `multi-prefix` cap).
|
/// Every prefix char this member holds, high→low (for the `multi-prefix` cap).
|
||||||
pub fn all_prefixes(&self) -> String {
|
pub fn all_prefixes(&self) -> String {
|
||||||
use crate::modules::customprefix::sigil;
|
use crate::modules::customprefix::sigil;
|
||||||
let mut s = String::new();
|
if self.custom_prefixes.is_empty() {
|
||||||
for (on, i) in [
|
let mut s = String::new();
|
||||||
(self.oprefix, 0),
|
for (on, i) in [
|
||||||
(self.owner, 1),
|
(self.oprefix, 0),
|
||||||
(self.admin, 2),
|
(self.owner, 1),
|
||||||
(self.op, 3),
|
(self.admin, 2),
|
||||||
(self.halfop, 4),
|
(self.op, 3),
|
||||||
(self.voice, 5),
|
(self.halfop, 4),
|
||||||
] {
|
(self.voice, 5),
|
||||||
if on {
|
] {
|
||||||
s.push_str(sigil(i));
|
if on {
|
||||||
|
s.push_str(sigil(i));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
self.held().iter().map(|(_, s)| *s).collect()
|
||||||
}
|
}
|
||||||
s
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,8 @@ impl Ircd {
|
||||||
crate::modules::metadata::load(&mut server); // restore channel metadata
|
crate::modules::metadata::load(&mut server); // restore channel metadata
|
||||||
crate::modules::reputation::load(&mut server); // restore per-IP reputation
|
crate::modules::reputation::load(&mut server); // restore per-IP reputation
|
||||||
crate::modules::geoip::init(&mut server); // load the GeoIP database
|
crate::modules::geoip::init(&mut server); // load the GeoIP database
|
||||||
crate::modules::customprefix::init(&server); // load prefix-sigil overrides
|
crate::modules::customprefix::init(&server); // load prefix config
|
||||||
|
crate::mode::init_custom_prefixes(); // register any config-defined prefix modes
|
||||||
Ircd {
|
Ircd {
|
||||||
server,
|
server,
|
||||||
commands: command_table(),
|
commands: command_table(),
|
||||||
|
|
|
||||||
40
src/mode.rs
40
src/mode.rs
|
|
@ -6,9 +6,11 @@
|
||||||
//! edit to the parser. The handler set is an ordinary slice of zero-sized
|
//! edit to the parser. The handler set is an ordinary slice of zero-sized
|
||||||
//! `&'static` values: no fixed cap, no per-mode allocation, no mutable registry.
|
//! `&'static` values: no fixed cap, no per-mode allocation, no mutable registry.
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use crate::channels::{
|
use crate::channels::{
|
||||||
normalize_ban_mask, Ban, ChanModes, Channel, MsgFlood, Rate, RANK_ADMIN, RANK_HALFOP, RANK_OP,
|
normalize_ban_mask, Ban, ChanModes, Channel, MsgFlood, Rate, RANK_ADMIN, RANK_HALFOP, RANK_OP,
|
||||||
RANK_OWNER,
|
RANK_OWNER, RANK_VOICE,
|
||||||
};
|
};
|
||||||
use crate::numeric::*;
|
use crate::numeric::*;
|
||||||
use crate::server::{now, Server};
|
use crate::server::{now, Server};
|
||||||
|
|
@ -44,9 +46,31 @@ pub trait ChanMode: Sync {
|
||||||
) -> Applied;
|
) -> Applied;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up the handler for a channel-mode letter.
|
/// Dynamic handlers for config-defined custom prefix modes (see modules::customprefix).
|
||||||
|
static CUSTOM_PREFIX_HANDLERS: OnceLock<Vec<Prefix>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Build the custom-prefix handlers once, at boot, after `customprefix::init`.
|
||||||
|
pub fn init_custom_prefixes() {
|
||||||
|
let v: Vec<Prefix> = crate::modules::customprefix::custom_defs()
|
||||||
|
.iter()
|
||||||
|
.map(|d| Prefix {
|
||||||
|
ch: d.letter,
|
||||||
|
rank: d.rank,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let _ = CUSTOM_PREFIX_HANDLERS.set(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up the handler for a channel-mode letter (built-in, then custom prefixes).
|
||||||
pub fn chan_mode(c: char) -> Option<&'static (dyn ChanMode + Sync)> {
|
pub fn chan_mode(c: char) -> Option<&'static (dyn ChanMode + Sync)> {
|
||||||
CHAN_MODES.iter().copied().find(|m| m.letter() == c)
|
if let Some(m) = CHAN_MODES.iter().copied().find(|m| m.letter() == c) {
|
||||||
|
return Some(m);
|
||||||
|
}
|
||||||
|
CUSTOM_PREFIX_HANDLERS
|
||||||
|
.get()?
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.ch == c)
|
||||||
|
.map(|p| p as &(dyn ChanMode + Sync))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The registered channel modes. Add a mode by adding its handler here.
|
/// The registered channel modes. Add a mode by adding its handler here.
|
||||||
|
|
@ -121,7 +145,7 @@ static HALFOP: Prefix = Prefix {
|
||||||
};
|
};
|
||||||
static VOICE: Prefix = Prefix {
|
static VOICE: Prefix = Prefix {
|
||||||
ch: 'v',
|
ch: 'v',
|
||||||
rank: 1, // RANK_VOICE
|
rank: RANK_VOICE,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl ChanMode for Prefix {
|
impl ChanMode for Prefix {
|
||||||
|
|
@ -189,13 +213,7 @@ impl ChanMode for Prefix {
|
||||||
.get_mut(key)
|
.get_mut(key)
|
||||||
.and_then(|c| c.members.get_mut(&tuid))
|
.and_then(|c| c.members.get_mut(&tuid))
|
||||||
{
|
{
|
||||||
match self.rank {
|
m.set_prefix(self.ch, adding); // routes built-in booleans + custom prefixes
|
||||||
RANK_OWNER => m.owner = adding,
|
|
||||||
RANK_ADMIN => m.admin = adding,
|
|
||||||
RANK_OP => m.op = adding,
|
|
||||||
RANK_HALFOP => m.halfop = adding,
|
|
||||||
_ => m.voice = adding,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// +D delayjoin: gaining a prefix reveals a hidden member
|
// +D delayjoin: gaining a prefix reveals a hidden member
|
||||||
if adding {
|
if adding {
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,61 @@
|
||||||
//! customprefix — reconfigure the channel prefix tiers from config, like InspIRCd's
|
//! customprefix — reconfigure the channel prefix tiers *and* define brand-new ones,
|
||||||
//! m_customprefix does for existing prefixes (`change="yes"`). One line per tier:
|
//! like InspIRCd's m_customprefix. Two forms, one line each:
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
|
//! # reconfigure a built-in tier (oper founder admin op halfop voice):
|
||||||
//! customprefix = op * ranktoset=admin ranktounset=admin depriv=no
|
//! customprefix = op * ranktoset=admin ranktounset=admin depriv=no
|
||||||
//! customprefix = voice -
|
//!
|
||||||
|
//! # define a NEW prefix mode (name is anything that isn't a built-in tier):
|
||||||
|
//! customprefix = helper letter=V prefix=? rank=25 ranktoset=op ranktounset=op depriv=yes
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Tiers: `oper founder admin op halfop voice`. Knobs:
|
//! For a built-in tier: a bare token is the sigil; `ranktoset`/`ranktounset` set the
|
||||||
//! * a bare token = the displayed sigil (e.g. `*`)
|
//! min rank to grant/revoke it; `depriv=no` forbids self-removal. For a new prefix:
|
||||||
//! * `ranktoset=<rank>` min rank to grant this prefix (default: the prefix's rank)
|
//! `letter` (mode char) and `prefix` (sigil) are required; `rank` is its rank
|
||||||
//! * `ranktounset=<rank>` min rank to revoke it (default: ranktoset)
|
//! (default 1); `ranktoset`/`ranktounset` default to `rank`; `depriv` defaults yes.
|
||||||
//! * `depriv=no` members may not remove this prefix from themselves
|
//! A `<rank>` is a number or a built-in tier name. New prefixes flow through the
|
||||||
//!
|
//! normal mode machinery (a dynamic handler is registered in [`crate::mode`]) and
|
||||||
//! A `<rank>` is a number (1–6) or a tier name (`op`, `admin`, …). Only the display
|
//! are held on `Member.custom_prefixes`. A linked network must share this config.
|
||||||
//! 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;
|
||||||
|
|
||||||
|
use crate::config::yesish;
|
||||||
use crate::server::Server;
|
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'];
|
pub 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
|
/// Built-in tier ranks (×10 spacing leaves room to slot custom tiers between them).
|
||||||
|
pub const RANKS: [u8; 6] = [60, 50, 40, 30, 20, 10];
|
||||||
|
|
||||||
|
/// A config-defined channel prefix mode.
|
||||||
|
pub struct PrefixDef {
|
||||||
|
pub letter: char,
|
||||||
|
pub sigil: String,
|
||||||
|
pub rank: u8,
|
||||||
|
pub ranktoset: u8,
|
||||||
|
pub ranktounset: u8,
|
||||||
|
pub depriv: bool,
|
||||||
|
}
|
||||||
|
|
||||||
struct PrefixCfg {
|
struct PrefixCfg {
|
||||||
sigils: [String; 6],
|
sigils: [String; 6],
|
||||||
ranktoset: [Option<u8>; 6],
|
ranktoset: [Option<u8>; 6],
|
||||||
ranktounset: [Option<u8>; 6],
|
ranktounset: [Option<u8>; 6],
|
||||||
depriv: [bool; 6],
|
depriv: [bool; 6],
|
||||||
|
custom: Vec<PrefixDef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
static CFG: OnceLock<PrefixCfg> = OnceLock::new();
|
static CFG: OnceLock<PrefixCfg> = OnceLock::new();
|
||||||
|
|
||||||
/// Parse a rank: a number (1–6) or a tier name.
|
fn is_builtin_letter(c: char) -> bool {
|
||||||
|
LETTERS.contains(&c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a rank: a number or a tier name.
|
||||||
fn parse_rank(v: &str) -> Option<u8> {
|
fn parse_rank(v: &str) -> Option<u8> {
|
||||||
if let Ok(n) = v.parse::<u8>() {
|
if let Ok(n) = v.parse::<u8>() {
|
||||||
return Some(n.min(6));
|
return Some(n);
|
||||||
}
|
}
|
||||||
TIER_NAMES
|
TIER_NAMES
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -47,82 +63,141 @@ fn parse_rank(v: &str) -> Option<u8> {
|
||||||
.map(|i| RANKS[i])
|
.map(|i| RANKS[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the `customprefix` overrides once, at boot.
|
/// Load the `customprefix` config once, at boot.
|
||||||
pub fn init(s: &Server) {
|
pub fn init(s: &Server) {
|
||||||
let mut cfg = PrefixCfg {
|
let mut cfg = PrefixCfg {
|
||||||
sigils: DEFAULT_SIGILS.map(String::from),
|
sigils: DEFAULT_SIGILS.map(String::from),
|
||||||
ranktoset: [None; 6],
|
ranktoset: [None; 6],
|
||||||
ranktounset: [None; 6],
|
ranktounset: [None; 6],
|
||||||
depriv: [true; 6],
|
depriv: [true; 6],
|
||||||
|
custom: Vec::new(),
|
||||||
};
|
};
|
||||||
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();
|
||||||
let Some(name) = it.next() else { continue };
|
let Some(name) = it.next() else { continue };
|
||||||
let Some(i) = TIER_NAMES.iter().position(|t| t.eq_ignore_ascii_case(name)) else {
|
if let Some(i) = TIER_NAMES.iter().position(|t| t.eq_ignore_ascii_case(name)) {
|
||||||
continue;
|
// reconfigure a built-in tier
|
||||||
};
|
for tok in it {
|
||||||
for tok in it {
|
match tok.split_once('=') {
|
||||||
match tok.split_once('=') {
|
Some(("ranktoset", v)) => cfg.ranktoset[i] = parse_rank(v),
|
||||||
Some(("ranktoset", v)) => cfg.ranktoset[i] = parse_rank(v),
|
Some(("ranktounset", v)) => cfg.ranktounset[i] = parse_rank(v),
|
||||||
Some(("ranktounset", v)) => cfg.ranktounset[i] = parse_rank(v),
|
Some(("depriv", v)) => cfg.depriv[i] = yesish(v),
|
||||||
Some(("depriv", v)) => cfg.depriv[i] = crate::config::yesish(v),
|
Some(_) => {}
|
||||||
Some(_) => {}
|
None => {
|
||||||
None => {
|
if let Some(c) = tok.chars().next() {
|
||||||
// a bare token is the sigil
|
cfg.sigils[i] = c.to_string();
|
||||||
if let Some(c) = tok.chars().next() {
|
}
|
||||||
cfg.sigils[i] = c.to_string();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// define a new prefix mode
|
||||||
|
let (mut letter, mut sigil, mut rank, mut rts, mut rtu, mut depriv) =
|
||||||
|
(None, None, 1u8, None, None, true);
|
||||||
|
for tok in it {
|
||||||
|
match tok.split_once('=') {
|
||||||
|
Some(("letter", v)) => letter = v.chars().next(),
|
||||||
|
Some(("prefix", v)) => sigil = v.chars().next(),
|
||||||
|
Some(("rank", v)) => rank = v.parse().unwrap_or(1),
|
||||||
|
Some(("ranktoset", v)) => rts = parse_rank(v),
|
||||||
|
Some(("ranktounset", v)) => rtu = parse_rank(v),
|
||||||
|
Some(("depriv", v)) => depriv = yesish(v),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let (Some(l), Some(sy)) = (letter, sigil) {
|
||||||
|
// don't shadow a built-in mode letter or a duplicate custom one
|
||||||
|
let taken = is_builtin_letter(l)
|
||||||
|
|| crate::mode::chan_mode(l).is_some()
|
||||||
|
|| cfg.custom.iter().any(|d| d.letter == l);
|
||||||
|
if !taken {
|
||||||
|
let ranktoset = rts.unwrap_or(rank);
|
||||||
|
cfg.custom.push(PrefixDef {
|
||||||
|
letter: l,
|
||||||
|
sigil: sy.to_string(),
|
||||||
|
rank,
|
||||||
|
ranktoset,
|
||||||
|
ranktounset: rtu.unwrap_or(ranktoset),
|
||||||
|
depriv,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = CFG.set(cfg);
|
let _ = CFG.set(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The sigil for tier `i` (0 = oper … 5 = voice).
|
/// The sigil for built-in tier `i` (0 = oper … 5 = voice).
|
||||||
pub fn sigil(i: usize) -> &'static str {
|
pub fn sigil(i: usize) -> &'static str {
|
||||||
CFG.get()
|
CFG.get()
|
||||||
.map(|c| c.sigils[i].as_str())
|
.map(|c| c.sigils[i].as_str())
|
||||||
.unwrap_or(DEFAULT_SIGILS[i])
|
.unwrap_or(DEFAULT_SIGILS[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every config-defined (non-built-in) prefix.
|
||||||
|
pub fn custom_defs() -> &'static [PrefixDef] {
|
||||||
|
CFG.get().map(|c| c.custom.as_slice()).unwrap_or(&[])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A custom prefix by its mode letter.
|
||||||
|
pub fn def_for_letter(c: char) -> Option<&'static PrefixDef> {
|
||||||
|
custom_defs().iter().find(|d| d.letter == c)
|
||||||
|
}
|
||||||
|
|
||||||
/// The prefix mode letter for a sigil char (FJOIN decode); `' '` if none.
|
/// The prefix mode letter for a sigil char (FJOIN decode); `' '` if none.
|
||||||
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)
|
if let Some(i) = (0..6).find(|&i| sigil(i) == cs) {
|
||||||
.find(|&i| sigil(i) == cs)
|
return LETTERS[i];
|
||||||
.map(|i| LETTERS[i])
|
}
|
||||||
|
custom_defs()
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.sigil == cs)
|
||||||
|
.map(|d| d.letter)
|
||||||
.unwrap_or(' ')
|
.unwrap_or(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
fn index_for_letter(letter: char) -> Option<usize> {
|
fn builtin_index(letter: char) -> Option<usize> {
|
||||||
LETTERS.iter().position(|&l| l == letter)
|
LETTERS.iter().position(|&l| l == letter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configured minimum rank to grant the prefix with mode letter `letter` (None ⇒
|
/// Min rank to grant a prefix (None ⇒ use the prefix's own rank).
|
||||||
/// use the prefix's own rank).
|
|
||||||
pub fn rank_to_set(letter: char) -> Option<u8> {
|
pub fn rank_to_set(letter: char) -> Option<u8> {
|
||||||
let i = index_for_letter(letter)?;
|
if let Some(i) = builtin_index(letter) {
|
||||||
CFG.get()?.ranktoset[i]
|
return CFG.get().and_then(|c| c.ranktoset[i]);
|
||||||
|
}
|
||||||
|
def_for_letter(letter).map(|d| d.ranktoset)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configured minimum rank to revoke the prefix (None ⇒ use the prefix's own rank).
|
/// Min rank to revoke a prefix (None ⇒ use the prefix's own rank).
|
||||||
pub fn rank_to_unset(letter: char) -> Option<u8> {
|
pub fn rank_to_unset(letter: char) -> Option<u8> {
|
||||||
let i = index_for_letter(letter)?;
|
if let Some(i) = builtin_index(letter) {
|
||||||
CFG.get()?.ranktounset[i]
|
return CFG.get().and_then(|c| c.ranktounset[i]);
|
||||||
|
}
|
||||||
|
def_for_letter(letter).map(|d| d.ranktounset)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a member may remove this prefix from themselves (default yes).
|
/// Whether a member may remove this prefix from themselves (default yes).
|
||||||
pub fn can_depriv(letter: char) -> bool {
|
pub fn can_depriv(letter: char) -> bool {
|
||||||
index_for_letter(letter)
|
if let Some(i) = builtin_index(letter) {
|
||||||
.and_then(|i| CFG.get().map(|c| c.depriv[i]))
|
return CFG.get().map(|c| c.depriv[i]).unwrap_or(true);
|
||||||
.unwrap_or(true)
|
}
|
||||||
|
def_for_letter(letter).map(|d| d.depriv).unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ISUPPORT `PREFIX=(modes)symbols` token; `include_oper` adds the `y` tier.
|
/// The ISUPPORT `PREFIX=(modes)symbols` token — built-in tiers plus custom prefixes,
|
||||||
|
/// ordered high→low by rank. `include_oper` adds the `y` (operprefix) tier.
|
||||||
pub fn isupport(include_oper: bool) -> String {
|
pub fn isupport(include_oper: bool) -> String {
|
||||||
|
let mut all: Vec<(u8, char, &'static str)> = Vec::new();
|
||||||
let start = if include_oper { 0 } else { 1 };
|
let start = if include_oper { 0 } else { 1 };
|
||||||
let letters: String = LETTERS[start..].iter().collect();
|
for i in start..6 {
|
||||||
let sigils: String = (start..6).map(sigil).collect();
|
all.push((RANKS[i], LETTERS[i], sigil(i)));
|
||||||
|
}
|
||||||
|
for d in custom_defs() {
|
||||||
|
all.push((d.rank, d.letter, d.sigil.as_str()));
|
||||||
|
}
|
||||||
|
all.sort_by(|a, b| b.0.cmp(&a.0));
|
||||||
|
let letters: String = all.iter().map(|(_, l, _)| *l).collect();
|
||||||
|
let sigils: String = all.iter().map(|(_, _, s)| *s).collect();
|
||||||
format!("({letters}){sigils}")
|
format!("({letters}){sigils}")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue