autoop: +w <prefix>:<mask> channel list mode grants status on join

This commit is contained in:
Jean Chevronnet 2026-08-10 08:18:07 +00:00
parent 87e3d5436d
commit 9d84a66437
8 changed files with 70 additions and 3 deletions

View file

@ -282,6 +282,7 @@ pub struct Channel {
pub invex: Vec<Ban>, // +I invite exceptions
pub filters: Vec<Ban>, // +g word/glob message filters (mask = the glob)
pub exemptchanops: Vec<Ban>, // +X exemptions (mask = "restriction:rankchar")
pub autoop: Vec<Ban>, // +w auto-status (mask = "prefixchar:hostmask")
pub invites: HashSet<Uid>, // uids allowed past +i
pub created: u64,
// --- ephemeral flood counters (not modes; never rendered or synced) -------
@ -311,6 +312,7 @@ impl Channel {
invex: Vec::new(),
filters: Vec::new(),
exemptchanops: Vec::new(),
autoop: Vec::new(),
invites: HashSet::new(),
created: now(),
msgflood_hits: HashMap::new(),

View file

@ -93,6 +93,7 @@ static CHAN_MODES: &[&(dyn ChanMode + Sync)] = &[
&DELAYMSG,
&REPEAT,
&EXEMPTCHANOPS,
&AUTOOP,
&DELAYJOIN,
];
@ -492,6 +493,7 @@ enum ListKind {
Invex,
Filter, // +g — message word/glob filters (not host masks)
ExemptChanOps, // +X — "restriction:rankchar" exemptions
AutoOp, // +w — "prefixchar:hostmask" auto-status on join
}
impl ListKind {
fn list<'a>(&self, c: &'a Channel) -> &'a Vec<Ban> {
@ -501,6 +503,7 @@ impl ListKind {
ListKind::Invex => &c.invex,
ListKind::Filter => &c.filters,
ListKind::ExemptChanOps => &c.exemptchanops,
ListKind::AutoOp => &c.autoop,
}
}
fn list_mut<'a>(&self, c: &'a mut Channel) -> &'a mut Vec<Ban> {
@ -510,6 +513,7 @@ impl ListKind {
ListKind::Invex => &mut c.invex,
ListKind::Filter => &mut c.filters,
ListKind::ExemptChanOps => &mut c.exemptchanops,
ListKind::AutoOp => &mut c.autoop,
}
}
/// (per-entry numeric, end-of-list numeric, name for the "End of …" line)
@ -524,6 +528,7 @@ impl ListKind {
RPL_ENDOFEXEMPTIONLIST,
"exemptchanops list",
),
ListKind::AutoOp => (RPL_AUTOOPLIST, RPL_ENDOFAUTOOP, "autoop list"),
}
}
/// Ban-style lists hold host masks and get filled out to `nick!user@host`;
@ -557,6 +562,10 @@ static EXEMPTCHANOPS: ListMode = ListMode {
ch: 'X',
kind: ListKind::ExemptChanOps,
};
static AUTOOP: ListMode = ListMode {
ch: 'w',
kind: ListKind::AutoOp,
};
impl ChanMode for ListMode {
fn letter(&self) -> char {
@ -1259,7 +1268,7 @@ mod tests {
#[test]
fn registry_covers_all_channel_modes() {
for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAPJUdKXD".chars() {
for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAPJUdKXwD".chars() {
assert!(chan_mode(c).is_some(), "missing handler for +{c}");
}
assert!(chan_mode('y').is_none());

47
src/modules/autoop.rs Normal file
View file

@ -0,0 +1,47 @@
//! autoop — the channel list mode `+w <prefix>:<hostmask>` grants a status prefix
//! to matching users the moment they join, e.g. `+w o:*!*@trusted.host` auto-ops
//! them, `+w v:*!*@*.friend` auto-voices. The list lives on the channel (like +b,
//! stored verbatim); this module applies it on join via the server-authority mode
//! path. Reference: InspIRCd's `m_autoop`. Original native Rust.
use crate::channels::glob_match;
use crate::module::Module;
use crate::server::Server;
use crate::Uid;
pub struct AutoOp;
impl Module for AutoOp {
fn name(&self) -> &'static str {
"autoop"
}
fn on_join(&mut self, s: &mut Server, uid: Uid, chan: &str) {
let key = chan.to_ascii_lowercase();
let Some(who) = s.users.get(&uid).map(|u| u.prefix()) else {
return;
};
let Some(nick) = s.users.get(&uid).map(|u| u.nick.clone()) else {
return;
};
// gather the prefix modes this user's mask earns (deduped)
let mut modes = String::new();
if let Some(c) = s.channels.get(&key) {
for e in &c.autoop {
let Some((pfx, mask)) = e.mask.split_once(':') else {
continue;
};
let Some(m) = pfx.chars().next() else { continue };
if "qaohv".contains(m) && !modes.contains(m) && glob_match(mask, &who) {
modes.push(m);
}
}
}
if modes.is_empty() {
return;
}
// grant them under server authority (the joiner can't op themselves)
let args: Vec<String> = modes.chars().map(|_| nick.clone()).collect();
crate::coremods::core_mode::svs_set_chan_modes(s, chan, &format!("+{modes}"), &args);
}
}

View file

@ -6,6 +6,7 @@
pub mod account_registration;
pub mod antimixedutf8;
pub mod antirandom;
pub mod autoop;
pub mod banredirect;
pub mod blockamsg;
pub mod channames;
@ -90,6 +91,7 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
Box::new(maphide::MapHide),
Box::new(dccallow::DccAllow),
Box::new(solvemsg::SolveMsg),
Box::new(autoop::AutoOp),
]
}

View file

@ -35,6 +35,8 @@ pub const ERR_BADCHANNEL: u16 = 926; // CBAN — this channel name is forbidden
pub const RPL_ENDOFSPAMFILTER: u16 = 940; // end of the +g word-filter list
pub const RPL_EXEMPTIONLIST: u16 = 954; // +X exemptchanops entry
pub const RPL_ENDOFEXEMPTIONLIST: u16 = 953; // end of the +X list
pub const RPL_AUTOOPLIST: u16 = 910; // +w autoop entry
pub const RPL_ENDOFAUTOOP: u16 = 911; // end of the +w list
pub const RPL_SPAMFILTER: u16 = 941; // one +g word-filter entry
pub const RPL_KNOCK: u16 = 710; // channel gets the knock
pub const RPL_KNOCKDLVR: u16 = 711; // knocker's ack

View file

@ -596,7 +596,7 @@ impl Server {
let maxnick = self.conf_num("maxnick", 30usize);
let maxchan = self.conf_num("maxchannel", 50usize);
let mut lines = vec![format!(
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgX,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,Gcgjmnrsy WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}",
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgXw,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,Gcgjmnrsy WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}",
self.network
)];
if let Some(tok) = crate::modules::network_icon::isupport(self) {

View file

@ -465,7 +465,7 @@ impl Server {
uid,
RPL_MYINFO,
&format!(
"{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJUdKXD",
"{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJUdKXwD",
self.name
),
);