From 9d84a664379e780700861e92f471ff2963360639 Mon Sep 17 00:00:00 2001 From: reverse Date: Mon, 10 Aug 2026 08:18:07 +0000 Subject: [PATCH] autoop: +w : channel list mode grants status on join --- echoircd.conf.example | 5 +++++ src/channels.rs | 2 ++ src/mode.rs | 11 +++++++++- src/modules/autoop.rs | 47 +++++++++++++++++++++++++++++++++++++++++++ src/modules/mod.rs | 2 ++ src/numeric.rs | 2 ++ src/server.rs | 2 +- src/users.rs | 2 +- 8 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 src/modules/autoop.rs diff --git a/echoircd.conf.example b/echoircd.conf.example index eefaa9f..273e7e0 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -120,6 +120,11 @@ amu_target = both # --- chanlog (m_chanlog): mirror the oper server-notice stream into a channel so # staff can watch it in a normal window. Set the channel (create/keep it opped): # chanlog = #snotices +# --- autoop (m_autoop): no config needed — it's the channel list mode +w. Grant a +# status prefix to matching users on join, `+w :`, e.g. +# /MODE #chan +w o:*!*@trusted.host (auto-op) +# /MODE #chan +w v:*!*@*.friend.net (auto-voice) +# /MODE #chan +w lists the entries. # --- banredirect (m_banredirect): no config needed — it extends ban syntax. A # ban `+b $<#channel>` bounces a matching user into #channel instead of # refusing them, e.g. /MODE #main +b *!*@*.spammer.net$#quarantine diff --git a/src/channels.rs b/src/channels.rs index a46fe22..e9ba574 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -282,6 +282,7 @@ pub struct Channel { pub invex: Vec, // +I invite exceptions pub filters: Vec, // +g word/glob message filters (mask = the glob) pub exemptchanops: Vec, // +X exemptions (mask = "restriction:rankchar") + pub autoop: Vec, // +w auto-status (mask = "prefixchar:hostmask") pub invites: HashSet, // 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(), diff --git a/src/mode.rs b/src/mode.rs index 7dfcd70..2b0fbc2 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -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 { @@ -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 { @@ -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()); diff --git a/src/modules/autoop.rs b/src/modules/autoop.rs new file mode 100644 index 0000000..83d2e4b --- /dev/null +++ b/src/modules/autoop.rs @@ -0,0 +1,47 @@ +//! autoop — the channel list mode `+w :` 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 = modes.chars().map(|_| nick.clone()).collect(); + crate::coremods::core_mode::svs_set_chan_modes(s, chan, &format!("+{modes}"), &args); + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index cb82e91..64346ae 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -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::new(maphide::MapHide), Box::new(dccallow::DccAllow), Box::new(solvemsg::SolveMsg), + Box::new(autoop::AutoOp), ] } diff --git a/src/numeric.rs b/src/numeric.rs index 47c841c..0f494e5 100644 --- a/src/numeric.rs +++ b/src/numeric.rs @@ -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 diff --git a/src/server.rs b/src/server.rs index 00f220f..fe8b655 100644 --- a/src/server.rs +++ b/src/server.rs @@ -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) { diff --git a/src/users.rs b/src/users.rs index 56fba67..5f19f29 100644 --- a/src/users.rs +++ b/src/users.rs @@ -465,7 +465,7 @@ impl Server { uid, RPL_MYINFO, &format!( - "{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJUdKXD", + "{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJUdKXwD", self.name ), );