//! IRC operator commands: OPER, KILL, WALLOPS, the SA*/SVS* set, X-lines, and //! the oper CHG*/SET* tools. Oper blocks are configured with `oper = name pass`. use crate::channels::Topic; use crate::command::{CmdResult, Command}; use crate::coremods::core_mode::{apply_mode, svs_set_user_modes}; use crate::module::Hook; use crate::numeric::*; use crate::server::{iso_time, now, Server}; use crate::users::{valid_host, valid_ident, valid_nick}; use crate::xline::{parse_duration, XKind}; use crate::Uid; pub fn commands() -> Vec> { vec![ Box::new(Oper), Box::new(Kill), Box::new(Wallops), Box::new(SvsLogin), Box::new(SvsLogout), Box::new(SvsNick), Box::new(SvsJoin), Box::new(SvsPart), Box::new(SvsMode), Box::new(GlobOps), Box::new(SaJoin), Box::new(SaPart), Box::new(SaNick), Box::new(Die), Box::new(Restart), Box::new(Kline), Box::new(Gline), Box::new(Zline), Box::new(Eline), Box::new(Shun), Box::new(Qline), Box::new(Cban), Box::new(Rline), Box::new(Connect), Box::new(ChgHost), Box::new(ChgIdent), Box::new(SetHost), Box::new(SetIdent), Box::new(SaMode), Box::new(SaTopic), Box::new(SaKick), Box::new(SaQuit), Box::new(ChgName), Box::new(ClearChan), Box::new(Check), Box::new(AllTime), Box::new(SwhoisCmd), Box::new(SetIdle), Box::new(NickLock), Box::new(NickUnlock), Box::new(OperMotd), ] } /// OPERMOTD — show the IRC-operators' message of the day, configured with /// repeated `opermotd = ` entries. struct OperMotd; impl Command for OperMotd { fn name(&self) -> &'static str { "OPERMOTD" } fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let nick = oper_nick(s, uid); let motd = s.conf_all("opermotd").to_vec(); if motd.is_empty() { s.send( uid, format!(":{} NOTICE {nick} :No OPERMOTD is set", s.name), ); return CmdResult::Ok; } s.send( uid, format!( ":{} NOTICE {nick} :- IRC Operators Message of the Day -", s.name ), ); for line in motd { s.send(uid, format!(":{} NOTICE {nick} :- {line}", s.name)); } s.send( uid, format!(":{} NOTICE {nick} :- End of OPERMOTD -", s.name), ); CmdResult::Ok } } /// An oper-set WHOIS line, stored per-user in `User.ext` and rendered by WHOIS /// (RPL_WHOISSPECIAL 320). pub struct Swhois(pub String); /// Reject non-opers with 481; returns whether the caller is an oper. fn require_oper(s: &mut Server, uid: Uid) -> bool { if s.is_oper(uid) { return true; } s.numeric( uid, ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator", ); false } struct Oper; impl Command for Oper { fn name(&self) -> &'static str { "OPER" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let (name, pass) = (params[0].clone(), params[1].clone()); let Some((hash, level)) = s .opers .iter() .find(|(n, _, _)| *n == name) .map(|(_, p, lvl)| (p.clone(), *lvl)) else { s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect"); return CmdResult::Fail; }; // bcrypt is slow — verify it off the core thread (result arrives as OperAuth). if hash.starts_with("$2") { if !s.spawn_auth(uid, hash, pass, level) { s.numeric(uid, ERR_PASSWDMISMATCH, ":Too many auth attempts, try again"); return CmdResult::Fail; } return CmdResult::Ok; // pending; oper-up happens when the verify returns } // fast hashes (plaintext / sha* / pbkdf2) verify inline if crate::modules::password_hash::verify(&hash, &pass) { s.oper_up(uid); crate::modules::operlevels::set(s, uid, level); // operlevels: KILL protection CmdResult::Ok } else { s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect"); CmdResult::Fail } } } struct Kill; impl Command for Kill { fn name(&self) -> &'static str { "KILL" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !s.is_oper(uid) { s.numeric( uid, ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator", ); return CmdResult::Fail; } let target = ¶ms[0]; let reason = params .get(1) .cloned() .unwrap_or_else(|| "Killed".to_string()); let Some(tuid) = s.find_nick(target) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{target} :No such nick/channel"), ); return CmdResult::Fail; }; // operlevels: a lower-level oper can't KILL a higher-level oper if let Some(reason) = crate::modules::operlevels::deny_kill(s, uid, tuid) { s.numeric(uid, ERR_NOPRIVILEGES, &format!(":{reason}")); return CmdResult::Fail; } let killer = s .users .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_default(); s.send( tuid, format!(":{} KILL {target} :{killer} ({reason})", s.name), ); s.remove_user(tuid, &format!("Killed by {killer}: {reason}")); CmdResult::Ok } } /// SVSLOGIN / SVSLOGOUT — the services interface to the account layer /// ([`crate::accounts`]). Over S2S these arrive from a services pseudoserver; /// until S2S exists an oper may invoke them to drive `+r` and the account-gated /// channel modes. `SVSLOGIN ` logs a user in (`account` of `*`/`0` /// logs out); `SVSLOGOUT ` logs them out. struct SvsLogin; impl Command for SvsLogin { fn name(&self) -> &'static str { "SVSLOGIN" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !s.is_oper(uid) { s.numeric( uid, ERR_NOPRIVILEGES, ":Permission Denied- SVSLOGIN is a services command", ); return CmdResult::Fail; } let (target, account) = (¶ms[0], ¶ms[1]); let Some(tuid) = s.find_nick(target) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{target} :No such nick/channel"), ); return CmdResult::Fail; }; if account == "*" || account == "0" { s.logout(tuid); } else { s.set_login(tuid, account); } CmdResult::Ok } } struct SvsLogout; impl Command for SvsLogout { fn name(&self) -> &'static str { "SVSLOGOUT" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !s.is_oper(uid) { s.numeric( uid, ERR_NOPRIVILEGES, ":Permission Denied- SVSLOGOUT is a services command", ); return CmdResult::Fail; } let Some(tuid) = s.find_nick(¶ms[0]) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{} :No such nick/channel", params[0]), ); return CmdResult::Fail; }; s.logout(tuid); CmdResult::Ok } } struct Wallops; impl Command for Wallops { fn name(&self) -> &'static str { "WALLOPS" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !s.is_oper(uid) { s.numeric( uid, ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator", ); return CmdResult::Fail; } let from = s .users .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_default(); s.wallops(&from, ¶ms[0]); CmdResult::Ok } } /// GLOBOPS — a message to every IRC operator. struct GlobOps; impl Command for GlobOps { fn name(&self) -> &'static str { "GLOBOPS" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let from = s .users .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_default(); let opers: Vec = s .users .iter() .filter(|(_, u)| u.flags.oper) .map(|(&u, _)| u) .collect(); for o in opers { let nick = s.users.get(&o).map(|u| u.nick.clone()).unwrap_or_default(); s.send( o, format!( ":{} NOTICE {nick} :*** GLOBOPS from {from}: {}", s.name, params[0] ), ); } CmdResult::Ok } } /// SAJOIN — force a user into a channel. struct SaJoin; impl Command for SaJoin { fn name(&self) -> &'static str { "SAJOIN" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(tuid) = s.find_nick(¶ms[0]) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{} :No such nick/channel", params[0]), ); return CmdResult::Fail; }; s.join(tuid, ¶ms[1], None); CmdResult::Ok } } /// SAPART — force a user out of a channel. struct SaPart; impl Command for SaPart { fn name(&self) -> &'static str { "SAPART" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(tuid) = s.find_nick(¶ms[0]) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{} :No such nick/channel", params[0]), ); return CmdResult::Fail; }; let reason = params .get(2) .cloned() .unwrap_or_else(|| "Removed".to_string()); s.force_part(tuid, ¶ms[1], &reason); CmdResult::Ok } } /// SANICK — force a user's nickname. struct SaNick; impl Command for SaNick { fn name(&self) -> &'static str { "SANICK" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(tuid) = s.find_nick(¶ms[0]) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{} :No such nick/channel", params[0]), ); return CmdResult::Fail; }; let newnick = ¶ms[1]; if !valid_nick(newnick, s.conf_num("maxnick", 30usize)) { s.numeric( uid, ERR_ERRONEUSNICKNAME, &format!("{newnick} :Erroneous nickname"), ); return CmdResult::Fail; } if s.find_nick(newnick).is_some() || s.remote_nick.contains_key(&newnick.to_ascii_lowercase()) { s.numeric( uid, ERR_NICKNAMEINUSE, &format!("{newnick} :Nickname is already in use"), ); return CmdResult::Fail; } s.set_nick(tuid, newnick); CmdResult::Ok } } // --- SVS* : the services interface. Same enforcement as the SA* oper commands, // under the names a services package speaks. Gated to opers/services; a linked // services pseudoserver drives these once S2S routes them. /// SVSNICK — force a nick change (nick-registration enforcement). An optional /// third param is the new-nick TS, accepted and ignored (single-TS model). struct SvsNick; impl Command for SvsNick { fn name(&self) -> &'static str { "SVSNICK" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(tuid) = s.find_nick(¶ms[0]) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{} :No such nick/channel", params[0]), ); return CmdResult::Fail; }; let newnick = ¶ms[1]; if !valid_nick(newnick, s.conf_num("maxnick", 30usize)) { s.numeric( uid, ERR_ERRONEUSNICKNAME, &format!("{newnick} :Erroneous nickname"), ); return CmdResult::Fail; } if s.find_nick(newnick).is_some() || s.remote_nick.contains_key(&newnick.to_ascii_lowercase()) { s.numeric( uid, ERR_NICKNAMEINUSE, &format!("{newnick} :Nickname is already in use"), ); return CmdResult::Fail; } s.set_nick(tuid, newnick); CmdResult::Ok } } /// SVSJOIN — force a user into a channel, bypassing +i/+k/+l/+b. struct SvsJoin; impl Command for SvsJoin { fn name(&self) -> &'static str { "SVSJOIN" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(tuid) = s.find_nick(¶ms[0]) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{} :No such nick/channel", params[0]), ); return CmdResult::Fail; }; s.join(tuid, ¶ms[1], None); CmdResult::Ok } } /// SVSPART — force a user out of a channel. struct SvsPart; impl Command for SvsPart { fn name(&self) -> &'static str { "SVSPART" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(tuid) = s.find_nick(¶ms[0]) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{} :No such nick/channel", params[0]), ); return CmdResult::Fail; }; let reason = params .get(2) .cloned() .unwrap_or_else(|| "Services forced part".to_string()); s.force_part(tuid, ¶ms[1], &reason); CmdResult::Ok } } /// SVSMODE — set modes on a user (e.g. `+r` registered) or a channel with services /// authority, bypassing the "own modes only" / rank checks a normal MODE enforces. struct SvsMode; impl Command for SvsMode { fn name(&self) -> &'static str { "SVSMODE" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } if params[0].starts_with('#') { // channel modes with services authority — same path as SAMODE s.mode_sudo = true; let r = apply_mode(s, uid, params); s.mode_sudo = false; return r; } let Some(tuid) = s.find_nick(¶ms[0]) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{} :No such nick/channel", params[0]), ); return CmdResult::Fail; }; svs_set_user_modes(s, tuid, ¶ms[1]); CmdResult::Ok } } /// DIE — shut the server down (requires the server name as confirmation). struct Die; impl Command for Die { fn name(&self) -> &'static str { "DIE" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } if params[0] != s.name { s.numeric(uid, ERR_NOPRIVILEGES, ":DIE requires the server name"); return CmdResult::Fail; } let by = s .users .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_default(); eprintln!("[oper] DIE by {by}"); std::process::exit(0); } } /// RESTART — like DIE (a supervisor is expected to relaunch us). struct Restart; impl Command for Restart { fn name(&self) -> &'static str { "RESTART" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } if params[0] != s.name { s.numeric(uid, ERR_NOPRIVILEGES, ":RESTART requires the server name"); return CmdResult::Fail; } let by = s .users .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_default(); eprintln!("[oper] RESTART by {by}"); std::process::exit(0); } } /// Shared KLINE/GLINE/ZLINE handling: the mask alone removes, mask+duration adds. fn do_xline(s: &mut Server, uid: Uid, params: &[String], kind: XKind) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let mask = params[0].clone(); let nick = s .users .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_default(); if params.len() < 2 { let word = if s.remove_xline(kind, &mask) { "removed" } else { "not found" }; s.send( uid, format!( ":{} NOTICE {nick} :{}-line {word}: {mask}", s.name, kind.tag() ), ); return CmdResult::Ok; } let dur = parse_duration(¶ms[1]).unwrap_or(0); let reason = params .get(2) .cloned() .unwrap_or_else(|| "No reason given".to_string()); s.add_xline(kind, &mask, dur, &nick, &reason); CmdResult::Ok } /// KLINE — ban a `user@host` mask on this server. struct Kline; impl Command for Kline { fn name(&self) -> &'static str { "KLINE" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { do_xline(s, uid, params, XKind::Kline) } } /// GLINE — a network-wide `user@host` ban. struct Gline; impl Command for Gline { fn name(&self) -> &'static str { "GLINE" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { do_xline(s, uid, params, XKind::Gline) } } /// ZLINE — ban an IP address (glob). struct Zline; impl Command for Zline { fn name(&self) -> &'static str { "ZLINE" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { do_xline(s, uid, params, XKind::Zline) } } /// ELINE — exempt a `user@host` / ip glob from all K/G/Z-lines. struct Eline; impl Command for Eline { fn name(&self) -> &'static str { "ELINE" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { do_xline(s, uid, params, XKind::Eline) } } /// SHUN — let a `user@host` connect but silently drop their commands. struct Shun; impl Command for Shun { fn name(&self) -> &'static str { "SHUN" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { do_xline(s, uid, params, XKind::Shun) } } /// QLINE — reserve/forbid a nick glob (opers bypass it). struct Qline; impl Command for Qline { fn name(&self) -> &'static str { "QLINE" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { do_xline(s, uid, params, XKind::Qline) } } /// CBAN — forbid a channel-name glob (opers bypass it). Mask alone removes; a /// mask + duration adds. struct Cban; impl Command for Cban { fn name(&self) -> &'static str { "CBAN" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { do_xline(s, uid, params, XKind::Cban) } } /// RLINE — ban users whose `nick!user@host realname` matches a regular expression. /// `RLINE [] :` adds; `RLINE ` removes. struct Rline; impl Command for Rline { fn name(&self) -> &'static str { "RLINE" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let pattern = params[0].clone(); let nick = s .users .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_default(); if params.len() < 2 { let word = if s.remove_xline(XKind::Rline, &pattern) { "removed" } else { "not found" }; s.send( uid, format!(":{} NOTICE {nick} :R-line {word}: {pattern}", s.name), ); return CmdResult::Ok; } if let Err(e) = crate::regex::Regex::new(&pattern) { s.send( uid, format!(":{} NOTICE {nick} :Invalid RLINE regex: {e}", s.name), ); return CmdResult::Fail; } let dur = parse_duration(¶ms[1]).unwrap_or(0); let reason = params .get(2) .cloned() .unwrap_or_else(|| "No reason given".to_string()); s.add_xline(XKind::Rline, &pattern, dur, &nick, &reason); s.enforce_rline(&pattern, &reason); CmdResult::Ok } } /// NICKLOCK — force a user's nick and lock it so they can't change it. /// `NICKLOCK `; opers/services still can. struct NickLock; impl Command for NickLock { fn name(&self) -> &'static str { "NICKLOCK" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(tuid) = oper_target(s, uid, ¶ms[0]) else { return CmdResult::Fail; }; let newnick = ¶ms[1]; if !valid_nick(newnick, s.conf_num("maxnick", 30usize)) { s.numeric( uid, ERR_ERRONEUSNICKNAME, &format!("{newnick} :Erroneous nickname"), ); return CmdResult::Fail; } let cur = s .users .get(&tuid) .map(|u| u.nick.clone()) .unwrap_or_default(); if !newnick.eq_ignore_ascii_case(&cur) { if s.find_nick(newnick).is_some() || s.remote_nick.contains_key(&newnick.to_ascii_lowercase()) { s.numeric( uid, ERR_NICKNAMEINUSE, &format!("{newnick} :Nickname is already in use"), ); return CmdResult::Fail; } s.set_nick(tuid, newnick); } if let Some(u) = s.users.get_mut(&tuid) { u.flags.nick_locked = true; } let by = oper_nick(s, uid); s.snotice(&format!("{by} used NICKLOCK on {newnick}")); CmdResult::Ok } } /// NICKUNLOCK — release a NICKLOCK so the user may change nick again. struct NickUnlock; impl Command for NickUnlock { fn name(&self) -> &'static str { "NICKUNLOCK" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(tuid) = oper_target(s, uid, ¶ms[0]) else { return CmdResult::Fail; }; if let Some(u) = s.users.get_mut(&tuid) { u.flags.nick_locked = false; } let by = oper_nick(s, uid); s.snotice(&format!("{by} used NICKUNLOCK on {}", params[0])); CmdResult::Ok } } /// CONNECT — dial a configured server link on demand. `CONNECT `. struct Connect; impl Command for Connect { fn name(&self) -> &'static str { "CONNECT" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let name = ¶ms[0]; let Some(b) = s .link_blocks .iter() .find(|b| b.name.eq_ignore_ascii_case(name)) .cloned() else { onotice(s, uid, &format!("CONNECT: no link block named {name}")); return CmdResult::Fail; }; if s.servers .values() .any(|sv| sv.name.eq_ignore_ascii_case(&b.name)) { onotice(s, uid, &format!("CONNECT: {} is already linked", b.name)); return CmdResult::Fail; } let addr = format!("{}:{}", b.ip, b.port); let (tx, counter) = (s.event_tx.clone(), s.conn_counter.clone()); let max_line = s.conf_num("max_line", crate::socketengine::DEFAULT_MAX_LINE); std::thread::spawn(move || crate::socketengine::connect_link(&addr, tx, counter, max_line)); let by = oper_nick(s, uid); s.snotice(&format!( "{by} used CONNECT to {} ({}:{})", b.name, b.ip, b.port )); CmdResult::Ok } } /// Resolve a nick to a uid, sending ERR_NOSUCHNICK if it's unknown. fn oper_target(s: &mut Server, uid: Uid, nick: &str) -> Option { match s.find_nick(nick) { Some(t) => Some(t), None => { s.numeric( uid, ERR_NOSUCHNICK, &format!("{nick} :No such nick/channel"), ); None } } } /// A server NOTICE to the invoking oper (soft errors for the CHG*/SA* set). fn onotice(s: &mut Server, uid: Uid, msg: &str) { let nick = s .users .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_else(|| "*".to_string()); s.send(uid, format!(":{} NOTICE {nick} :{msg}", s.name)); } /// The oper's nick, for audit snotices. fn oper_nick(s: &Server, uid: Uid) -> String { s.users .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_default() } /// CHGHOST — change another user's displayed host. struct ChgHost; impl Command for ChgHost { fn name(&self) -> &'static str { "CHGHOST" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } if !valid_host(¶ms[1]) { onotice(s, uid, "*** CHGHOST: invalid characters in hostname"); return CmdResult::Fail; } let Some(t) = oper_target(s, uid, ¶ms[0]) else { return CmdResult::Fail; }; s.change_host_ident(t, None, Some(¶ms[1])); let by = oper_nick(s, uid); s.snotice(&format!( "{by} used CHGHOST on {}: {}", params[0], params[1] )); CmdResult::Ok } } /// SETHOST — change your own displayed host. struct SetHost; impl Command for SetHost { fn name(&self) -> &'static str { "SETHOST" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } if !valid_host(¶ms[0]) { onotice(s, uid, "*** SETHOST: invalid characters in hostname"); return CmdResult::Fail; } s.change_host_ident(uid, None, Some(¶ms[0])); CmdResult::Ok } } /// CHGIDENT — change another user's ident/username. struct ChgIdent; impl Command for ChgIdent { fn name(&self) -> &'static str { "CHGIDENT" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } if !valid_ident(¶ms[1]) { onotice(s, uid, "*** CHGIDENT: invalid characters in ident"); return CmdResult::Fail; } let Some(t) = oper_target(s, uid, ¶ms[0]) else { return CmdResult::Fail; }; s.change_host_ident(t, Some(¶ms[1]), None); let by = oper_nick(s, uid); s.snotice(&format!( "{by} used CHGIDENT on {}: {}", params[0], params[1] )); CmdResult::Ok } } /// SETIDENT — change your own ident/username. struct SetIdent; impl Command for SetIdent { fn name(&self) -> &'static str { "SETIDENT" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } if !valid_ident(¶ms[0]) { onotice(s, uid, "*** SETIDENT: invalid characters in ident"); return CmdResult::Fail; } s.change_host_ident(uid, Some(¶ms[0]), None); CmdResult::Ok } } /// SAMODE — apply a channel MODE as the server, bypassing the rank ladder. struct SaMode; impl Command for SaMode { fn name(&self) -> &'static str { "SAMODE" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } s.mode_sudo = true; let r = apply_mode(s, uid, params); s.mode_sudo = false; let by = oper_nick(s, uid); s.snotice(&format!("{by} used SAMODE: {}", params.join(" "))); r } } /// SATOPIC — set a channel topic as the server, bypassing +t / op checks. struct SaTopic; impl Command for SaTopic { fn name(&self) -> &'static str { "SATOPIC" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let chan = ¶ms[0]; let key = chan.to_ascii_lowercase(); if !s.channels.contains_key(&key) { s.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{chan} :No such channel")); return CmdResult::Fail; } let text = params[1].clone(); let (prefix, setter) = { let u = &s.users[&uid]; (u.prefix(), u.nick.clone()) }; if let Some(ch) = s.channels.get_mut(&key) { ch.topic = Some(Topic { text: text.clone(), setter, ts: now(), }); } s.to_channel(&key, &format!(":{prefix} TOPIC {chan} :{text}"), None); s.propagate_from_user(uid, &format!("TOPIC {chan} :{text}")); let by = oper_nick(s, uid); s.snotice(&format!("{by} used SATOPIC on {chan}")); CmdResult::Ok } } /// SAKICK — kick a user as the server, bypassing rank checks. struct SaKick; impl Command for SaKick { fn name(&self) -> &'static str { "SAKICK" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let (chan, victim) = (¶ms[0], ¶ms[1]); let key = chan.to_ascii_lowercase(); if !s.channels.contains_key(&key) { s.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{chan} :No such channel")); return CmdResult::Fail; } let Some(tuid) = s.find_nick(victim) else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{victim} :No such nick/channel"), ); return CmdResult::Fail; }; if !s.channels[&key].members.contains_key(&tuid) { s.numeric( uid, ERR_USERNOTINCHANNEL, &format!("{victim} {chan} :They aren't on that channel"), ); return CmdResult::Fail; } let reason = params .get(2) .cloned() .unwrap_or_else(|| "Kicked by services".to_string()); let prefix = s.users[&uid].prefix(); s.to_channel( &key, &format!(":{prefix} KICK {chan} {victim} :{reason}"), None, ); s.propagate_from_user(uid, &format!("KICK {chan} {victim} :{reason}")); if let Some(ch) = s.channels.get_mut(&key) { ch.members.remove(&tuid); } if let Some(u) = s.users.get_mut(&tuid) { u.channels.remove(&key); } s.channels.retain(|_, c| c.keep_alive()); s.events .push_back(Hook::Part(tuid, key, "kicked".to_string())); let by = oper_nick(s, uid); s.snotice(&format!("{by} used SAKICK on {victim} in {chan}")); CmdResult::Ok } } /// SAQUIT — force a user to quit the network. Looks to everyone like a normal /// client QUIT. struct SaQuit; impl Command for SaQuit { fn name(&self) -> &'static str { "SAQUIT" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(tuid) = oper_target(s, uid, ¶ms[0]) else { return CmdResult::Fail; }; let reason = params .get(1) .cloned() .unwrap_or_else(|| "Services forced quit".to_string()); s.send(tuid, format!("ERROR :Closing link: (SAQUIT: {reason})")); s.remove_user(tuid, &format!("Quit: {reason}")); let by = oper_nick(s, uid); s.snotice(&format!("{by} used SAQUIT on {}: {reason}", params[0])); CmdResult::Ok } } /// CHGNAME — change another user's real name (the oper-driven counterpart to /// SETNAME); broadcast to `setname`-capable peers so clients update live. struct ChgName; impl Command for ChgName { fn name(&self) -> &'static str { "CHGNAME" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(t) = oper_target(s, uid, ¶ms[0]) else { return CmdResult::Fail; }; let realname = params[1].clone(); let prefix = match s.users.get_mut(&t) { Some(u) => { u.realname = realname.clone(); u.prefix() } None => return CmdResult::Fail, }; let line = format!(":{prefix} SETNAME :{realname}"); if s.users.get(&t).map(|u| u.caps.setname).unwrap_or(false) { s.send(t, line.clone()); } s.notify_peers(t, &line, |c| c.setname); let by = oper_nick(s, uid); s.snotice(&format!("{by} used CHGNAME on {}: {realname}", params[0])); CmdResult::Ok } } /// CLEARCHAN — kick every user out of a channel. Each removal is a normal KICK, /// propagated like SAKICK. struct ClearChan; impl Command for ClearChan { fn name(&self) -> &'static str { "CLEARCHAN" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let chan = params[0].clone(); let key = chan.to_ascii_lowercase(); if !s.channels.contains_key(&key) { s.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{chan} :No such channel")); return CmdResult::Fail; } let reason = params .get(1) .cloned() .unwrap_or_else(|| "Channel cleared by services".to_string()); let prefix = s.users[&uid].prefix(); let members: Vec = s.channels[&key].members.keys().copied().collect(); for tuid in members { let victim = s .users .get(&tuid) .map(|u| u.nick.clone()) .unwrap_or_default(); if victim.is_empty() { continue; } s.to_channel( &key, &format!(":{prefix} KICK {chan} {victim} :{reason}"), None, ); s.propagate_from_user(uid, &format!("KICK {chan} {victim} :{reason}")); if let Some(ch) = s.channels.get_mut(&key) { ch.members.remove(&tuid); } if let Some(u) = s.users.get_mut(&tuid) { u.channels.remove(&key); } s.events .push_back(Hook::Part(tuid, key.clone(), "cleared".to_string())); } s.channels.retain(|_, c| c.keep_alive()); let by = oper_nick(s, uid); s.snotice(&format!("{by} used CLEARCHAN on {chan}")); CmdResult::Ok } } /// CHECK — oper diagnostic dump for a nick or channel. struct Check; impl Command for Check { fn name(&self) -> &'static str { "CHECK" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let target = params[0].clone(); if target.starts_with('#') { let key = target.to_ascii_lowercase(); let (count, topic, members) = match s.channels.get(&key) { Some(ch) => ( ch.members.len(), ch.topic .as_ref() .map(|t| t.text.clone()) .unwrap_or_default(), ch.members .keys() .filter_map(|m| s.users.get(m).map(|u| u.nick.clone())) .collect::>(), ), None => { s.numeric( uid, ERR_NOSUCHCHANNEL, &format!("{target} :No such channel"), ); return CmdResult::Fail; } }; onotice( s, uid, &format!("*** CHECK {target}: channel, {count} members"), ); if !topic.is_empty() { onotice(s, uid, &format!("*** topic: {topic}")); } onotice(s, uid, &format!("*** members: {}", members.join(" "))); } else if let Some(t) = s.find_nick(&target) { let lines = { let u = &s.users[&t]; vec![ format!("*** CHECK {target}: {}", u.prefix()), format!("*** realhost {} ip {}", u.host, u.addr.ip()), format!("*** realname: {}", u.realname), format!( "*** account: {} secure: {}", u.account.clone().unwrap_or_else(|| "*".to_string()), u.secure ), format!("*** umodes: +{}", u.flags.umodes()), format!( "*** signon {} idle {}s", iso_time(u.signon), now().saturating_sub(u.last_active) ), format!( "*** channels: {}", u.channels.iter().cloned().collect::>().join(" ") ), ] }; for l in lines { onotice(s, uid, &l); } } else { s.numeric( uid, ERR_NOSUCHNICK, &format!("{target} :No such nick/channel"), ); return CmdResult::Fail; } CmdResult::Ok } } /// SWHOIS — attach (or clear) an extra WHOIS line on a user. `SWHOIS /// :`; an empty text removes it. Shown as RPL_WHOISSPECIAL. struct SwhoisCmd; impl Command for SwhoisCmd { fn name(&self) -> &'static str { "SWHOIS" } fn min_params(&self) -> usize { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let Some(t) = oper_target(s, uid, ¶ms[0]) else { return CmdResult::Fail; }; // everything after the nick is the line — works with or without a `:` let text = params[1..].join(" "); if let Some(u) = s.users.get_mut(&t) { if text.is_empty() { u.ext.take::(); } else { u.ext.set(Swhois(text.clone())); } } let by = oper_nick(s, uid); s.snotice(&format!("{by} used SWHOIS on {}: {text}", params[0])); CmdResult::Ok } } /// SETIDLE — reset your own idle time. `SETIDLE ` backdates the /// last-activity clock so WHOIS shows that idle time. struct SetIdle; impl Command for SetIdle { fn name(&self) -> &'static str { "SETIDLE" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let secs: u64 = params[0].parse().unwrap_or(0); if let Some(u) = s.users.get_mut(&uid) { u.last_active = now().saturating_sub(secs); } onotice(s, uid, &format!("*** SETIDLE: idle time set to {secs}s")); CmdResult::Ok } } /// ALLTIME — show the current server time to the requesting oper (on a single /// server there's just the one time to report). struct AllTime; impl Command for AllTime { fn name(&self) -> &'static str { "ALLTIME" } fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult { if !require_oper(s, uid) { return CmdResult::Fail; } let msg = format!("ALLTIME: {} {}", s.name, iso_time(now())); onotice(s, uid, &msg); CmdResult::Ok } }