From 4d6525cac24683103d787aac8e28a103f67cd05f Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 01:07:30 +0000 Subject: [PATCH] =?UTF-8?q?opmoderated=20channel=20mode=20(+U=20=E2=80=94?= =?UTF-8?q?=20route=20unprivileged=20messages=20to=20ops)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/channels.rs | 2 ++ src/coremods/core_message.rs | 14 +++++++++++++- src/mode.rs | 10 +++++++++- src/users.rs | 4 ++-- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/channels.rs b/src/channels.rs index 81d14da..9b7c677 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -154,6 +154,7 @@ pub struct ChanModes { pub allowinvite: bool, // +A — any member (not just ops) may INVITE pub permanent: bool, // +P — channel persists with zero members pub kicknorejoin: Option, // +J — block rejoin for N secs after a kick + pub opmoderated: bool, // +U — unprivileged users' messages go to ops only } impl ChanModes { @@ -180,6 +181,7 @@ impl ChanModes { 'Q' => self.nokicks = on, 'A' => self.allowinvite = on, 'P' => self.permanent = on, + 'U' => self.opmoderated = on, _ => {} } } diff --git a/src/coremods/core_message.rs b/src/coremods/core_message.rs index 8ba226e..d522fff 100644 --- a/src/coremods/core_message.rs +++ b/src/coremods/core_message.rs @@ -176,13 +176,21 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) } return CmdResult::Fail; } + // +U opmoderated — an unprivileged user's message isn't blocked; it's routed + // to channel ops only (below). It also overrides +m's block for that purpose. + let op_only = s + .channels + .get(&key) + .map(|c| c.modes.opmoderated) + .unwrap_or(false) + && s.rank(uid, &key) < RANK_VOICE; // +m: only voiced-or-above may speak let moderated = s .channels .get(&key) .map(|c| c.modes.moderated) .unwrap_or(false); - if moderated && s.rank(uid, &key) < RANK_VOICE { + if moderated && s.rank(uid, &key) < RANK_VOICE && !op_only { if !notice { s.numeric( uid, @@ -347,6 +355,10 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) if m == uid || s.users.get(&m).map(|u| u.flags.deaf).unwrap_or(false) { continue; } + // +U: an unprivileged sender's message reaches ops (half-op+) only + if op_only && s.rank(m, &key) < RANK_HALFOP { + continue; + } s.send_tagged(m, uid, &ctags, &msgid, &line); } // echo-message: give the sender their own copy if they asked for one diff --git a/src/mode.rs b/src/mode.rs index c946698..be7a1af 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -89,6 +89,7 @@ static CHAN_MODES: &[&(dyn ChanMode + Sync)] = &[ &ALLOWINVITE, &PERMANENT, &KICKNOREJOIN, + &OPMODERATED, ]; // --- prefix modes (+q/+a/+o/+h/+v): a per-member rank, needs a nick ---------- @@ -307,6 +308,9 @@ fn set_allowinvite(m: &mut ChanModes, v: bool) { fn set_permanent(m: &mut ChanModes, v: bool) { m.permanent = v; } +fn set_opmoderated(m: &mut ChanModes, v: bool) { + m.opmoderated = v; +} static NOKICKS: Flag = Flag { ch: 'Q', set: set_nokicks, @@ -319,6 +323,10 @@ static PERMANENT: Flag = Flag { ch: 'P', set: set_permanent, }; +static OPMODERATED: Flag = Flag { + ch: 'U', + set: set_opmoderated, +}; impl ChanMode for Flag { fn letter(&self) -> char { @@ -1142,7 +1150,7 @@ mod tests { #[test] fn registry_covers_all_channel_modes() { - for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAPJ".chars() { + for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAPJU".chars() { assert!(chan_mode(c).is_some(), "missing handler for +{c}"); } assert!(chan_mode('y').is_none()); diff --git a/src/users.rs b/src/users.rs index 1699869..6fa32c6 100644 --- a/src/users.rs +++ b/src/users.rs @@ -417,7 +417,7 @@ impl Server { uid, RPL_MYINFO, &format!( - "{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJ", + "{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJU", self.name ), ); @@ -425,7 +425,7 @@ impl Server { uid, RPL_ISUPPORT, &format!( - "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFLHBJ,ACGMNOPQRSTcimnpstuz EXTBAN=,cmn WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server", + "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFLHBJ,ACGMNOPQRSTUcimnpstuz EXTBAN=,cmn WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server", self.network ), );