echoIRCd/src/coremods/core_mode.rs

389 lines
13 KiB
Rust

//! MODE: parse the modestring and dispatch each letter to its handler in
//! [`crate::mode`] (channel and user modes alike).
use crate::channels::RANK_HALFOP;
use crate::command::{CmdResult, Command};
use crate::mode::{chan_mode, user_mode, Applied};
use crate::numeric::*;
use crate::server::Server;
use crate::Uid;
pub fn commands() -> Vec<Box<dyn Command>> {
vec![Box::new(Mode)]
}
/// Append one mode change to the echo string, emitting the +/- only when it flips.
fn emit(applied: &mut String, last: &mut char, sign: char, c: char) {
if *last != sign {
applied.push(sign);
*last = sign;
}
applied.push(c);
}
/// Apply user modes to `tuid` with services authority (SVSMODE): no "only your
/// own modes" restriction — that's the whole point. Reuses the per-mode handlers
/// and broadcasts the result to the target as `:nick MODE nick :<changes>`.
pub fn svs_set_user_modes(s: &mut Server, tuid: Uid, modestring: &str) {
let mut sign = '+';
let mut applied = String::new();
let mut last = ' ';
s.mode_sudo = true; // services authority — allows server-only modes like +k
for c in modestring.chars() {
if c == '+' || c == '-' {
sign = c;
continue;
}
let adding = sign == '+';
if let Some(handler) = user_mode(c) {
if handler.apply(s, tuid, adding) {
emit(&mut applied, &mut last, sign, c);
}
}
}
s.mode_sudo = false;
if !applied.is_empty() {
let nick = s
.users
.get(&tuid)
.map(|u| u.nick.clone())
.unwrap_or_default();
s.send(tuid, format!(":{nick} MODE {nick} :{applied}"));
}
}
struct Mode;
impl Command for Mode {
fn name(&self) -> &'static str {
"MODE"
}
fn min_params(&self) -> usize {
1
}
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
apply_mode(s, uid, params)
}
}
/// The MODE body, shared with SAMODE (which wraps it in `Server::mode_sudo` so
/// the rank gates below all pass — see `core_oper::SaMode`).
pub fn apply_mode(s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
let target = &params[0];
if !target.starts_with('#') {
return apply_user_modes(s, uid, target, params);
}
let key = target.to_ascii_lowercase();
if !s.channels.contains_key(&key) {
s.numeric(
uid,
ERR_NOSUCHCHANNEL,
&format!("{target} :No such channel"),
);
return CmdResult::Fail;
}
// query: `MODE #c`
if params.len() < 2 {
let modestr = s.channels[&key].modes.render(s.is_member(uid, &key));
s.numeric(uid, RPL_CHANNELMODEIS, &format!("{target} {modestr}"));
let created = s.channels[&key].created;
s.numeric(uid, RPL_CREATIONTIME, &format!("{target} {created}"));
return CmdResult::Ok;
}
// dispatch each mode letter to its handler
let modestring = params[1].clone();
let args = &params[2..];
// a *pure list query* (only list-mode letters, no arguments, e.g. `MODE #c +b`)
// is just viewing — allow it for anyone (the hidelist module may still restrict
// it). Anything that changes a mode needs at least half-op; each handler then
// enforces its own finer rule (prefixes need enough rank, +z needs all-secure…).
let pure_list_query = args.is_empty()
&& modestring
.chars()
.filter(|c| *c != '+' && *c != '-')
.all(|c| chan_mode(c).is_some_and(|h| h.is_list()));
// Viewing autoop (+w), exemptchanops (+X) or filter (+g) exposes trusted
// host/pattern lists, so require half-op to read them (public ban lists stay open).
let sensitive_view = pure_list_query && modestring.chars().any(|c| matches!(c, 'w' | 'X' | 'g'));
if (!pure_list_query || sensitive_view) && s.rank(uid, &key) < RANK_HALFOP {
s.numeric(
uid,
ERR_CHANOPRIVSNEEDED,
&format!("{target} :You're not a channel operator"),
);
return CmdResult::Fail;
}
let mut argi = 0usize;
let mut sign = '+';
let mut applied = String::new();
let mut last = ' ';
let mut echoed: Vec<String> = Vec::new();
// (sign, letter, displayed-param) per applied change — for hidemode filtering
let mut changes: Vec<(char, char, Option<String>)> = Vec::new();
// Cap mode changes per command (advertised as MODES=, default 20 like InspIRCd):
// otherwise `MODE #c +bbbb…` in one line dispatches hundreds of handlers, each
// fanning out to the whole channel and every link — a cheap amplification flood.
let max_modes = s.conf_num("modes", 20usize).max(1);
let mut processed = 0usize;
for c in modestring.chars() {
if c == '+' || c == '-' {
sign = c;
continue;
}
if processed >= max_modes {
break;
}
processed += 1;
let adding = sign == '+';
let Some(handler) = chan_mode(c) else {
s.numeric(
uid,
ERR_UNKNOWNMODE,
&format!("{c} :is unknown mode char to me"),
);
continue;
};
let param = if handler.wants_param(adding) {
let p = args.get(argi).cloned();
if p.is_some() {
argi += 1;
}
p
} else {
None
};
if let Applied::Yes(echo) = handler.apply(s, target, &key, uid, adding, param.as_deref()) {
emit(&mut applied, &mut last, sign, c);
changes.push((sign, c, echo.clone()));
if let Some(p) = echo {
echoed.push(p);
}
}
}
if !applied.is_empty() {
let prefix = s.users[&uid].prefix();
let pstr = if echoed.is_empty() {
String::new()
} else {
format!(" {}", echoed.join(" "))
};
// hidemode: if any changed mode is configured hidden, deliver per-recipient
// so members below the required rank don't see it; the setter, opers and
// linked servers always get the full line.
if changes
.iter()
.any(|(_, c, _)| crate::modules::hidemode::hidden_rank(s, *c).is_some())
{
crate::modules::hidemode::broadcast(s, &key, target, uid, &prefix, &changes);
} else {
s.to_channel(
&key,
&format!(":{prefix} MODE {target} {applied}{pstr}"),
None,
);
}
// links: a timestamped FMODE sourced from the acting user's uuid
let src_uuid = s.users[&uid].uuid.clone();
s.propagate_chan_mode(&src_uuid, target, &applied, &echoed);
}
// A mode change may have removed the channel's last reason to exist while it has
// no members (e.g. -P / -r on an empty channel): destroy it now, as an empty
// channel is normally culled the moment its final member leaves.
let cull = s.channels.get(&key).is_some_and(|c| !c.keep_alive());
if cull {
s.channels.remove(&key);
}
CmdResult::Ok
}
/// Apply channel modes with **server** authority (no acting user) — for the RPC
/// `channel.set_mode`. Same per-letter dispatch as [`apply_mode`] under `mode_sudo`
/// (so every rank gate passes), but the resulting `MODE` line is sourced from the
/// server, not a user. The handlers only ever touch the actor uid through
/// `s.rank()` (maxed by sudo) or `s.users.get()` (safe on the `0` sentinel), so no
/// live actor is needed. Returns whether anything actually changed.
pub fn svs_set_chan_modes(s: &mut Server, target: &str, modestring: &str, args: &[String]) -> bool {
let key = target.to_ascii_lowercase();
if !s.channels.contains_key(&key) {
return false;
}
s.mode_sudo = true;
let mut argi = 0usize;
let mut sign = '+';
let mut applied = String::new();
let mut last = ' ';
let mut echoed: Vec<String> = Vec::new();
for c in modestring.chars() {
if c == '+' || c == '-' {
sign = c;
continue;
}
let adding = sign == '+';
let Some(handler) = chan_mode(c) else {
continue;
};
let param = if handler.wants_param(adding) {
let p = args.get(argi).cloned();
if p.is_some() {
argi += 1;
}
p
} else {
None
};
if let Applied::Yes(echo) = handler.apply(s, target, &key, 0, adding, param.as_deref()) {
emit(&mut applied, &mut last, sign, c);
if let Some(p) = echo {
echoed.push(p);
}
}
}
s.mode_sudo = false;
if applied.is_empty() {
return false;
}
let pstr = if echoed.is_empty() {
String::new()
} else {
format!(" {}", echoed.join(" "))
};
s.to_channel(
&key,
&format!(":{} MODE {target} {applied}{pstr}", s.name),
None,
);
// links: a timestamped FMODE sourced from this server's sid
let sid = s.sid.clone();
s.propagate_chan_mode(&sid, target, &applied, &echoed);
true
}
/// Apply a client `+s`/`-s` snomask change. Oper-only. `+s` with a mask edits the
/// subscribed categories (`+cq` adds, `-c` removes, `*` all); `+s` with no mask
/// subscribes to everything; `-s` clears it. Emits `RPL_SNOMASKIS` (008) and returns
/// whether the `s` mode char should appear in the MODE echo.
fn apply_snomask(s: &mut Server, uid: Uid, adding: bool, param: Option<&str>) -> bool {
if !s.is_oper(uid) {
s.numeric(
uid,
crate::numeric::ERR_NOPRIVILEGES,
":Permission denied - only operators may set a server notice mask",
);
return false;
}
let mut cats: std::collections::BTreeSet<char> = s
.users
.get(&uid)
.map(|u| u.flags.snomask_cats.chars().collect())
.unwrap_or_default();
let all = || crate::users::DEFAULT_SNOMASK.chars().collect::<std::collections::BTreeSet<char>>();
if !adding {
cats.clear();
} else {
match param {
None => cats = all(),
Some(p) => {
let mut sign = '+';
for c in p.chars() {
match c {
'+' => sign = '+',
'-' => sign = '-',
'*' => cats = if sign == '+' { all() } else { Default::default() },
c if crate::users::DEFAULT_SNOMASK.contains(c) => {
if sign == '+' {
cats.insert(c);
} else {
cats.remove(&c);
}
}
_ => {} // ignore unknown snomask letters
}
}
}
}
}
let mask: String = cats.iter().collect();
let on = !mask.is_empty();
if let Some(u) = s.users.get_mut(&uid) {
u.flags.snomask = on;
u.flags.snomask_cats = mask.clone();
}
s.numeric(
uid,
crate::numeric::RPL_SNOMASKIS,
&format!("+{mask} :Server notice mask"),
);
if adding {
on
} else {
true
}
}
/// User modes: dispatched to the [`crate::mode`] `UserMode` handler objects.
fn apply_user_modes(s: &mut Server, uid: Uid, target: &str, params: &[String]) -> CmdResult {
let me = s
.users
.get(&uid)
.map(|u| u.nick.clone())
.unwrap_or_default();
if !target.eq_ignore_ascii_case(&me) {
s.numeric(
uid,
ERR_USERSDONTMATCH,
":Can't change mode for other users",
);
return CmdResult::Ok;
}
if params.len() < 2 {
let um = s
.users
.get(&uid)
.map(|u| u.flags.umodes())
.unwrap_or_else(|| "+".to_string());
s.numeric(uid, RPL_UMODEIS, &um);
return CmdResult::Ok;
}
let modestring = params[1].clone();
let mut sign = '+';
let mut applied = String::new();
let mut last = ' ';
let mut argi = 2usize; // params[2..] are mode arguments (the +s snomask mask)
for c in modestring.chars() {
if c == '+' || c == '-' {
sign = c;
continue;
}
let adding = sign == '+';
// +s is a parametric snomask mode: it consumes the following mask argument
if c == 's' {
let param = if adding {
let p = params.get(argi).cloned();
if p.is_some() {
argi += 1;
}
p
} else {
None
};
if apply_snomask(s, uid, adding, param.as_deref()) {
emit(&mut applied, &mut last, sign, c);
}
continue;
}
let Some(handler) = user_mode(c) else {
s.numeric(
uid,
ERR_UMODEUNKNOWNFLAG,
&format!(":Unknown MODE flag {c}"),
);
continue;
};
if handler.apply(s, uid, adding) {
emit(&mut applied, &mut last, sign, c);
}
}
if !applied.is_empty() {
s.send(uid, format!(":{me} MODE {me} :{applied}"));
}
CmdResult::Ok
}