diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 862d6cf..4b28629 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -41,6 +41,7 @@ pub mod reputation; pub mod restrictchans; pub mod restrictcommands; pub mod restrictmsg; +pub mod rmode; pub mod rpc; pub mod securelist; pub mod securitygroups; @@ -109,5 +110,6 @@ pub fn module_commands() -> Vec> { .chain(filehost::commands()) .chain(extended_isupport::commands()) .chain(tline::commands()) + .chain(rmode::commands()) .collect() } diff --git a/src/modules/rmode.rs b/src/modules/rmode.rs new file mode 100644 index 0000000..64c3b4f --- /dev/null +++ b/src/modules/rmode.rs @@ -0,0 +1,105 @@ +//! rmode — `RMODE [pattern]`, bulk-remove entries from a +//! channel list mode (`b` bans, `e` ban exceptions, `I` invite exceptions). With a +//! `pattern` glob only matching entries are cleared; without one, all are. Needs +//! half-op+ (opers bypass). The removals go through the normal mode engine (chunked +//! to keep each `MODE` line legal), so they broadcast and propagate like any other. +//! +//! Behaviour reference: InspIRCd's `m_rmode`. Original native Rust. + +use crate::channels::{glob_match, RANK_HALFOP}; +use crate::command::{CmdResult, Command}; +use crate::coremods::core_mode::apply_mode; +use crate::numeric::{ERR_CHANOPRIVSNEEDED, ERR_NOSUCHCHANNEL}; +use crate::server::Server; +use crate::Uid; + +pub fn commands() -> Vec> { + vec![Box::new(RMode)] +} + +struct RMode; +impl Command for RMode { + fn name(&self) -> &'static str { + "RMODE" + } + fn min_params(&self) -> usize { + 2 + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + let chan = params[0].clone(); + let key = chan.to_ascii_lowercase(); + let Some(mode) = params[1].chars().find(|c| c.is_ascii_alphabetic()) else { + return CmdResult::Fail; + }; + if !s.channels.contains_key(&key) { + s.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{chan} :No such channel")); + return CmdResult::Fail; + } + let is_oper = s.is_oper(uid); + if !is_oper && s.rank(uid, &key) < RANK_HALFOP { + s.numeric( + uid, + ERR_CHANOPRIVSNEEDED, + &format!("{chan} :You're not a channel operator"), + ); + return CmdResult::Fail; + } + // collect the matching masks from the requested list mode + let pattern = params.get(2).map(|p| p.as_str()).unwrap_or("*"); + let masks: Vec = { + let ch = &s.channels[&key]; + let list = match mode { + 'b' => &ch.bans, + 'e' => &ch.excepts, + 'I' => &ch.invex, + _ => { + let nick = s + .users + .get(&uid) + .map(|u| u.nick.clone()) + .unwrap_or_default(); + s.send( + uid, + format!( + ":{} NOTICE {nick} :RMODE only supports list modes b, e, I", + s.name + ), + ); + return CmdResult::Fail; + } + }; + list.iter() + .filter(|b| glob_match(pattern, &b.mask)) + .map(|b| b.mask.clone()) + .collect() + }; + let removed = masks.len(); + // apply in chunks so each MODE line stays within limits + for chunk in masks.chunks(6) { + let mut p = vec![ + chan.clone(), + format!("-{}", mode.to_string().repeat(chunk.len())), + ]; + p.extend(chunk.iter().cloned()); + if is_oper { + s.mode_sudo = true; + } + apply_mode(s, uid, &p); + s.mode_sudo = false; + } + let nick = s + .users + .get(&uid) + .map(|u| u.nick.clone()) + .unwrap_or_default(); + let plural = if removed == 1 { "y" } else { "ies" }; + s.send( + uid, + format!( + ":{} NOTICE {nick} :RMODE: removed {removed} +{mode} entr{plural} from {chan}", + s.name + ), + ); + CmdResult::Ok + } +}