namedmodes: PROP command sets/queries channel modes by long name (op, moderated, limit, ...)

This commit is contained in:
Jean Chevronnet 2026-08-11 18:55:14 +00:00
parent 3b43abc88a
commit b4186ae08d
2 changed files with 150 additions and 0 deletions

View file

@ -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<Box<dyn Command>> {
.chain(globops::commands())
.chain(relaymsg::commands())
.chain(ojoin::commands())
.chain(namedmodes::commands())
.collect()
}

148
src/modules/namedmodes.rs Normal file
View file

@ -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<char> {
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<Box<dyn Command>> {
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<String> = Vec::new();
let mut i = 1;
while i < params.len() {
let tok = &params[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)
}
}