channels: replace Member's six parallel prefix bools (oprefix/owner/admin/op/halfop/voice) with a single u8 bitfield (PFX_*) + inline bool accessors/mutators — same semantics, one byte instead of six, no more risk of the flags drifting out of sync; all call sites go through op()/set_op()-style methods

This commit is contained in:
Jean Chevronnet 2026-08-19 02:47:04 +00:00
parent 16fae5b23c
commit 1aa02a08b9
6 changed files with 129 additions and 76 deletions

View file

@ -12,18 +12,24 @@ use crate::Uid;
/// Per-member prefix modes (+q/+a/+o/+h/+v). Flag modes live in [`ChanModes`].
#[derive(Default)]
pub struct Member {
pub oprefix: bool, // operprefix/ojoin: server oper prefix (!), highest rank
pub owner: bool, // +q (~)
pub admin: bool, // +a (&)
pub op: bool, // +o (@)
pub halfop: bool, // +h (%)
pub voice: bool, // +v (+)
/// Built-in prefix modes held, as a bitfield of `PFX_*` (was six parallel bools:
/// oprefix `!`, owner `~`, admin `&`, op `@`, halfop `%`, voice `+`). Read/write
/// through the `op()`/`set_op()`-style accessors below.
pub prefixes: u8,
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 recent_msgs: Vec<String>, // +K repeat: this member's last few lines here
pub hidden: bool, // +D delayjoin: JOIN withheld until they reveal themselves
}
/// Built-in prefix bits held in [`Member::prefixes`], high→low.
pub const PFX_OPER: u8 = 0b10_0000; // ! network staff (operprefix/ojoin)
pub const PFX_OWNER: u8 = 0b01_0000; // ~ +q
pub const PFX_ADMIN: u8 = 0b00_1000; // & +a
pub const PFX_OP: u8 = 0b00_0100; // @ +o
pub const PFX_HALFOP: u8 = 0b00_0010; // % +h
pub const PFX_VOICE: u8 = 0b00_0001; // + +v
/// Prefix ranks, high→low — gate who may grant a prefix / kick whom. Spaced ×10 so
/// config-defined custom prefixes (modules::customprefix) can slot in between.
pub const RANK_OPER: u8 = 60; // operprefix/ojoin — above channel owner (network staff)
@ -34,20 +40,71 @@ pub const RANK_HALFOP: u8 = 20;
pub const RANK_VOICE: u8 = 10;
impl Member {
#[inline]
pub fn oprefix(&self) -> bool {
self.prefixes & PFX_OPER != 0
}
#[inline]
pub fn owner(&self) -> bool {
self.prefixes & PFX_OWNER != 0
}
#[inline]
pub fn admin(&self) -> bool {
self.prefixes & PFX_ADMIN != 0
}
#[inline]
pub fn op(&self) -> bool {
self.prefixes & PFX_OP != 0
}
#[inline]
pub fn halfop(&self) -> bool {
self.prefixes & PFX_HALFOP != 0
}
#[inline]
pub fn voice(&self) -> bool {
self.prefixes & PFX_VOICE != 0
}
#[inline]
fn set_bit(&mut self, bit: u8, on: bool) {
if on {
self.prefixes |= bit;
} else {
self.prefixes &= !bit;
}
}
pub fn set_oprefix(&mut self, on: bool) {
self.set_bit(PFX_OPER, on);
}
pub fn set_owner(&mut self, on: bool) {
self.set_bit(PFX_OWNER, on);
}
pub fn set_admin(&mut self, on: bool) {
self.set_bit(PFX_ADMIN, on);
}
pub fn set_op(&mut self, on: bool) {
self.set_bit(PFX_OP, on);
}
pub fn set_halfop(&mut self, on: bool) {
self.set_bit(PFX_HALFOP, on);
}
pub fn set_voice(&mut self, on: bool) {
self.set_bit(PFX_VOICE, on);
}
/// This member's numeric rank (0 = plain member).
/// Built-in tier rank from the fixed booleans (0 = none), ignoring custom prefixes.
/// Built-in tier rank from the fixed prefix bits (0 = none), ignoring custom prefixes.
fn builtin_rank(&self) -> u8 {
if self.oprefix {
if self.oprefix() {
RANK_OPER
} else if self.owner {
} else if self.owner() {
RANK_OWNER
} else if self.admin {
} else if self.admin() {
RANK_ADMIN
} else if self.op {
} else if self.op() {
RANK_OP
} else if self.halfop {
} else if self.halfop() {
RANK_HALFOP
} else if self.voice {
} else if self.voice() {
RANK_VOICE
} else {
0
@ -70,12 +127,12 @@ impl Member {
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),
(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)));
@ -96,17 +153,17 @@ impl Member {
use crate::modules::customprefix::sigil;
if self.custom_prefixes.is_empty() {
// fast path: built-in tiers only
if self.oprefix {
if self.oprefix() {
sigil(0)
} else if self.owner {
} else if self.owner() {
sigil(1)
} else if self.admin {
} else if self.admin() {
sigil(2)
} else if self.op {
} else if self.op() {
sigil(3)
} else if self.halfop {
} else if self.halfop() {
sigil(4)
} else if self.voice {
} else if self.voice() {
sigil(5)
} else {
""
@ -121,21 +178,17 @@ impl Member {
/// Drop the standard status modes (q/a/o/h/v) — used when this side loses a
/// channel-timestamp war and every member must be de-statused.
pub fn clear_status(&mut self) {
self.owner = false;
self.admin = false;
self.op = false;
self.halfop = false;
self.voice = false;
self.prefixes &= !(PFX_OWNER | PFX_ADMIN | PFX_OP | PFX_HALFOP | PFX_VOICE);
}
pub fn set_prefix(&mut self, letter: char, on: bool) {
match letter {
'y' => self.oprefix = on,
'q' => self.owner = on,
'a' => self.admin = on,
'o' => self.op = on,
'h' => self.halfop = on,
'v' => self.voice = on,
'y' => self.set_bit(PFX_OPER, on),
'q' => self.set_bit(PFX_OWNER, on),
'a' => self.set_bit(PFX_ADMIN, on),
'o' => self.set_bit(PFX_OP, on),
'h' => self.set_bit(PFX_HALFOP, on),
'v' => self.set_bit(PFX_VOICE, on),
_ => {
if crate::modules::customprefix::def_for_letter(letter).is_some() {
self.custom_prefixes.retain(|&c| c != letter);
@ -153,12 +206,12 @@ impl Member {
if self.custom_prefixes.is_empty() {
let mut s = String::new();
for (on, i) in [
(self.oprefix, 0),
(self.owner, 1),
(self.admin, 2),
(self.op, 3),
(self.halfop, 4),
(self.voice, 5),
(self.oprefix(), 0),
(self.owner(), 1),
(self.admin(), 2),
(self.op(), 3),
(self.halfop(), 4),
(self.voice(), 5),
] {
if on {
s.push_str(sigil(i));
@ -177,12 +230,12 @@ impl Member {
use crate::modules::customprefix::def_for_letter;
let mut s = String::new();
for (on, l) in [
(self.oprefix, 'y'),
(self.owner, 'q'),
(self.admin, 'a'),
(self.op, 'o'),
(self.halfop, 'h'),
(self.voice, 'v'),
(self.oprefix(), 'y'),
(self.owner(), 'q'),
(self.admin(), 'a'),
(self.op(), 'o'),
(self.halfop(), 'h'),
(self.voice(), 'v'),
] {
if on {
s.push(l);
@ -898,7 +951,7 @@ impl Server {
ch.members.insert(
uid,
Member {
op: is_new,
prefixes: if is_new { PFX_OP } else { 0 },
joined: now(),
..Default::default()
},
@ -1607,15 +1660,15 @@ mod tests {
let mut m = Member::default();
assert_eq!(m.rank(), 0);
assert_eq!(m.prefix_char(), "");
m.voice = true;
m.set_voice(true);
assert_eq!((m.rank(), m.prefix_char()), (RANK_VOICE, "+"));
m.halfop = true;
m.set_halfop(true);
assert_eq!((m.rank(), m.prefix_char()), (RANK_HALFOP, "%"));
m.op = true;
m.set_op(true);
assert_eq!((m.rank(), m.prefix_char()), (RANK_OP, "@"));
m.admin = true;
m.set_admin(true);
assert_eq!((m.rank(), m.prefix_char()), (RANK_ADMIN, "&"));
m.owner = true;
m.set_owner(true);
assert_eq!((m.rank(), m.prefix_char()), (RANK_OWNER, "~"));
}
}

View file

@ -2526,8 +2526,8 @@ mod tests {
let msg = crate::message::parse(":42SB00000 IJOIN #echoircd 16 1 ao").unwrap();
s.link_ijoin_recv(1, &msg);
let m = &s.channels["#echoircd"].rmembers["42SB00000"];
assert!(m.admin, "bot should hold +a (&) from the IJOIN status token");
assert!(m.op, "bot should hold +o (@) from the IJOIN status token");
assert!(m.admin(), "bot should hold +a (&) from the IJOIN status token");
assert!(m.op(), "bot should hold +o (@) from the IJOIN status token");
}
// A server is a service iff its NAME matches the sasl_server or a `uline` config
@ -2565,7 +2565,7 @@ mod tests {
// a peer bursts #c with a NEWER TS, opping bob — bob must join WITHOUT +o
let m = crate::message::parse(":42S FJOIN #c 2000 +nt :o,42SAAAAAA").unwrap();
s.link_fjoin_recv(1, &m);
let opped = s.channels["#c"].rmembers["42SAAAAAA"].op;
let opped = s.channels["#c"].rmembers["42SAAAAAA"].op();
assert!(!opped, "a member bursted with a newer (losing) TS must be de-statused");
assert_eq!(s.channels["#c"].created, 1000, "our older TS is kept");
}

View file

@ -105,19 +105,19 @@ impl Command for ExtJwt {
let cmodes = member
.map(|m| {
let mut v = Vec::new();
if m.owner {
if m.owner() {
v.push('q');
}
if m.admin {
if m.admin() {
v.push('a');
}
if m.op {
if m.op() {
v.push('o');
}
if m.halfop {
if m.halfop() {
v.push('h');
}
if m.voice {
if m.voice() {
v.push('v');
}
v

View file

@ -17,8 +17,8 @@ fn set(s: &mut Server, uid: Uid, key: &str, on: bool) {
return;
};
let changed = match s.channels.get_mut(key).and_then(|c| c.members.get_mut(&uid)) {
Some(m) if m.oprefix != on => {
m.oprefix = on;
Some(m) if m.oprefix() != on => {
m.set_oprefix(on);
true
}
_ => false,

View file

@ -10,11 +10,11 @@ use crate::server::{now, Server};
fn prefixes(m: &crate::channels::Member) -> String {
let mut p = String::new();
for (has, ch) in [
(m.owner, '~'),
(m.admin, '&'),
(m.op, '@'),
(m.halfop, '%'),
(m.voice, '+'),
(m.owner(), '~'),
(m.admin(), '&'),
(m.op(), '@'),
(m.halfop(), '%'),
(m.voice(), '+'),
] {
if has {
p.push(ch);

View file

@ -1384,11 +1384,11 @@ impl Server {
.map(|mem| {
let mut s = String::new();
for (on, c) in [
(mem.owner, 'q'),
(mem.admin, 'a'),
(mem.op, 'o'),
(mem.halfop, 'h'),
(mem.voice, 'v'),
(mem.owner(), 'q'),
(mem.admin(), 'a'),
(mem.op(), 'o'),
(mem.halfop(), 'h'),
(mem.voice(), 'v'),
] {
if on {
s.push(c);
@ -1579,8 +1579,8 @@ mod tests {
s.join(1, "#c", None); // ann creates -> gets @
s.join(2, "#c", None); // bob joins
assert!(s.channels["#c"].members[&1].op);
assert!(!s.channels["#c"].members[&2].op);
assert!(s.channels["#c"].members[&1].op());
assert!(!s.channels["#c"].members[&2].op());
assert_eq!(s.channels["#c"].members.len(), 2);
let ann: Vec<String> = arx.try_iter().collect();