diff --git a/src/modules/mod.rs b/src/modules/mod.rs index fcf5bf6..a617353 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -46,6 +46,7 @@ pub mod maphide; pub mod markread; pub mod metadata; pub mod multiline; +pub mod namedmodes; pub mod network_icon; pub mod ojoin; pub mod operlevels; @@ -144,5 +145,6 @@ pub fn module_commands() -> Vec> { .chain(globops::commands()) .chain(relaymsg::commands()) .chain(ojoin::commands()) + .chain(namedmodes::commands()) .collect() } diff --git a/src/modules/namedmodes.rs b/src/modules/namedmodes.rs new file mode 100644 index 0000000..b1ac825 --- /dev/null +++ b/src/modules/namedmodes.rs @@ -0,0 +1,148 @@ +//! namedmodes — the `PROP` command sets or queries channel modes by long name +//! instead of by letter, e.g. `PROP #chan +op nick +moderated -limit`. Names map to +//! echoIRCd's channel-mode letters and go through the normal MODE machinery +//! (`core_mode::apply_mode`), so all the usual permission checks apply. `PROP #chan` +//! with no changes lists the modes currently set, by name. + +use crate::command::{CmdResult, Command}; +use crate::coremods::core_mode::apply_mode; +use crate::mode::chan_mode; +use crate::numeric::ERR_NOSUCHCHANNEL; +use crate::server::Server; +use crate::Uid; + +/// Mode long-name → echoIRCd channel-mode letter. +static NAMES: &[(&str, char)] = &[ + ("founder", 'q'), + ("admin", 'a'), + ("op", 'o'), + ("halfop", 'h'), + ("voice", 'v'), + ("ban", 'b'), + ("banexception", 'e'), + ("invex", 'I'), + ("key", 'k'), + ("limit", 'l'), + ("moderated", 'm'), + ("noextmsg", 'n'), + ("topiclock", 't'), + ("inviteonly", 'i'), + ("secret", 's'), + ("private", 'p'), + ("sslonly", 'z'), + ("operonly", 'O'), + ("nonick", 'N'), + ("noctcp", 'C'), + ("nonotice", 'T'), + ("blockcolor", 'c'), + ("stripcolor", 'S'), + ("censor", 'G'), + ("auditorium", 'u'), + ("regonly", 'R'), + ("regmoderated", 'M'), + ("nokicks", 'Q'), + ("allowinvite", 'A'), + ("permanent", 'P'), + ("opmoderated", 'U'), + ("delayjoin", 'D'), + ("filter", 'g'), + ("exemptchanops", 'X'), + ("autoop", 'w'), + ("flood", 'f'), + ("joinflood", 'j'), + ("nickflood", 'F'), + ("redirect", 'L'), + ("blockcaps", 'B'), + ("kicknorejoin", 'J'), + ("history", 'H'), + ("delaymsg", 'd'), + ("repeat", 'K'), +]; + +fn name_to_letter(name: &str) -> Option { + let n = name.to_ascii_lowercase(); + NAMES.iter().find(|(nm, _)| *nm == n).map(|(_, c)| *c) +} + +fn letter_to_name(letter: char) -> Option<&'static str> { + NAMES.iter().find(|(_, c)| *c == letter).map(|(nm, _)| *nm) +} + +pub fn commands() -> Vec> { + vec![Box::new(Prop)] +} + +struct Prop; +impl Command for Prop { + fn name(&self) -> &'static str { + "PROP" + } + fn min_params(&self) -> usize { + 1 + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + let target = params[0].clone(); + let key = target.to_ascii_lowercase(); + if !target.starts_with('#') || !s.channels.contains_key(&key) { + s.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{target} :No such channel")); + return CmdResult::Fail; + } + // no changes → list the modes currently set, by name + if params.len() < 2 { + let rendered = s.channels[&key].modes.render(s.is_member(uid, &key)); + let letters = rendered + .trim_start_matches(['+', '-']) + .split(' ') + .next() + .unwrap_or(""); + let names: Vec<&str> = letters.chars().filter_map(letter_to_name).collect(); + let nick = s + .users + .get(&uid) + .map(|u| u.nick.clone()) + .unwrap_or_default(); + s.send( + uid, + format!( + ":{} NOTICE {nick} :{target} modes: {} ({rendered})", + s.name, + names.join(" ") + ), + ); + return CmdResult::Ok; + } + // translate `+name [value] -name …` into a MODE string + ordered args, then + // reuse apply_mode (same wants_param arg-consumption, so they stay aligned) + let mut modestr = String::new(); + let mut modeargs: Vec = Vec::new(); + let mut i = 1; + while i < params.len() { + let tok = ¶ms[i]; + let (sign, nm) = match tok.chars().next() { + Some('+') => ('+', &tok[1..]), + Some('-') => ('-', &tok[1..]), + _ => { + i += 1; + continue; + } + }; + if let Some(letter) = name_to_letter(nm) { + modestr.push(sign); + modestr.push(letter); + if chan_mode(letter).is_some_and(|m| m.wants_param(sign == '+')) { + i += 1; + if i < params.len() { + modeargs.push(params[i].clone()); + } + } + } + i += 1; + } + if modestr.is_empty() { + return CmdResult::Ok; + } + let mut p = vec![target, modestr]; + p.extend(modeargs); + apply_mode(s, uid, &p) + } +}