//! core_extra — the standard informational / utility commands a full ircd is //! expected to answer: LIST, WHOWAS, USERHOST, ISON, TIME, ADMIN, INFO, STATS, MAP. use crate::command::{CmdResult, Command}; use crate::numeric::*; use crate::server::{iso_time, now, Server, VERSION}; use crate::xline::XKind; use crate::Uid; pub fn commands() -> Vec> { vec![ Box::new(List), Box::new(Whowas), Box::new(UserHost), Box::new(IsOn), Box::new(Time), Box::new(Admin), Box::new(Info), Box::new(Stats), Box::new(Map), Box::new(Help), ] } /// HELP [topic] — built-in help: a short index, or detail for a command group. struct Help; impl Command for Help { fn name(&self) -> &'static str { "HELP" } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let topic = params .first() .map(|t| t.to_ascii_uppercase()) .unwrap_or_default(); let (head, body): (&str, &[&str]) = match topic.as_str() { "" => ( "*", &[ "Use HELP for detail. Topics: CHANNELS SERVICES OPER.", "Channels: JOIN PART TOPIC MODE KICK INVITE NAMES LIST WHO WHOIS.", "Messaging: PRIVMSG, NOTICE, and AWAY to mark yourself away.", "Accounts: authenticate with SASL, or message NickServ (/NS).", ], ), "CHANNELS" => ( "CHANNELS", &[ "JOIN #chan [key] join (or create) a channel", "PART #chan [:reason] leave a channel", "MODE #chan [+modes] view or set channel modes", "TOPIC #chan :text set the topic (needs +t rights)", "KICK #chan nick remove a user (needs op/halfop)", "INVITE nick #chan invite a user to a channel", ], ), "SERVICES" => ( "SERVICES", &[ "Register/identify with NickServ: /NS REGISTER, /NS IDENTIFY.", "Channel ownership via ChanServ: /CS REGISTER #chan.", "Aliases /NS /CS /MS /OS /BS /HS message the matching service.", ], ), "OPER" => ( "OPER", &[ "OPER become an IRC operator", "KILL nick :reason disconnect a user", "KLINE/GLINE/ZLINE ban a mask (network bans propagate)", "SAMODE/SAJOIN/SAKICK act with services authority", "REHASH reload the config", ], ), other => { s.numeric( uid, RPL_HELPSTART, &format!("{other} :No help available for that topic"), ); s.numeric(uid, RPL_ENDOFHELP, &format!("{other} :End of /HELP")); return CmdResult::Ok; } }; s.numeric(uid, RPL_HELPSTART, &format!("{head} :{} help", s.name)); for line in body { s.numeric(uid, RPL_HELPTXT, &format!("{head} :{line}")); } s.numeric(uid, RPL_ENDOFHELP, &format!("{head} :End of /HELP")); CmdResult::Ok } } struct List; impl Command for List { fn name(&self) -> &'static str { "LIST" } fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult { s.numeric(uid, RPL_LISTSTART, "Channel :Users Name"); let keys: Vec = s.channels.keys().cloned().collect(); for key in keys { let ch = &s.channels[&key]; // hide secret / private channels from non-members if (ch.modes.secret || ch.modes.private) && !ch.members.contains_key(&uid) { continue; } let count = ch.members.len() + ch.rmembers.len(); let topic = ch .topic .as_ref() .map(|t| t.text.clone()) .unwrap_or_default(); let name = ch.name.clone(); s.numeric(uid, RPL_LIST, &format!("{name} {count} :{topic}")); } s.numeric(uid, RPL_LISTEND, ":End of /LIST"); CmdResult::Ok } } struct Whowas; impl Command for Whowas { fn name(&self) -> &'static str { "WHOWAS" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let target = ¶ms[0]; let want = target.to_ascii_lowercase(); let limit = params .get(1) .and_then(|c| c.parse::().ok()) .unwrap_or(8); let hits: Vec<(String, String, String, String, u64)> = s .whowas .iter() .filter(|e| e.nick.to_ascii_lowercase() == want) .take(limit) .map(|e| { ( e.nick.clone(), e.ident.clone(), e.host.clone(), e.realname.clone(), e.ts, ) }) .collect(); if hits.is_empty() { s.numeric( uid, ERR_WASNOSUCHNICK, &format!("{target} :There was no such nickname"), ); } for (nick, ident, host, realname, ts) in hits { s.numeric( uid, RPL_WHOWASUSER, &format!("{nick} {ident} {host} * :{realname}"), ); s.numeric( uid, RPL_WHOISSERVER, &format!("{nick} {} :{}", s.name, iso_time(ts)), ); } s.numeric(uid, RPL_ENDOFWHOWAS, &format!("{target} :End of WHOWAS")); CmdResult::Ok } } struct UserHost; impl Command for UserHost { fn name(&self) -> &'static str { "USERHOST" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let mut parts: Vec = Vec::new(); for nick in params.iter().take(5) { if let Some(tuid) = s.find_nick(nick) { if let Some(u) = s.users.get(&tuid) { let star = if u.flags.oper { "*" } else { "" }; let here = if u.flags.away.is_some() { "-" } else { "+" }; parts.push(format!( "{}{star}={here}{}@{}", u.nick, u.ident, u.host_display() )); } } } s.numeric(uid, RPL_USERHOST, &format!(":{}", parts.join(" "))); CmdResult::Ok } } struct IsOn; impl Command for IsOn { fn name(&self) -> &'static str { "ISON" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let on: Vec = params .iter() .flat_map(|p| p.split_whitespace()) .filter(|n| s.find_nick(n).is_some() || s.find_remote(n).is_some()) .map(|n| n.to_string()) .collect(); s.numeric(uid, RPL_ISON, &format!(":{}", on.join(" "))); CmdResult::Ok } } struct Time; impl Command for Time { fn name(&self) -> &'static str { "TIME" } fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult { s.numeric(uid, RPL_TIME, &format!("{} :{}", s.name, iso_time(now()))); CmdResult::Ok } } struct Admin; impl Command for Admin { fn name(&self) -> &'static str { "ADMIN" } fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult { s.numeric( uid, RPL_ADMINME, &format!("{} :Administrative info", s.name), ); s.numeric(uid, RPL_ADMINLOC1, &format!(":{} IRC network", s.network)); s.numeric( uid, RPL_ADMINLOC2, ":echoIRCd — a from-scratch ircd in Rust", ); s.numeric(uid, RPL_ADMINEMAIL, &format!(":admin@{}", s.name)); CmdResult::Ok } } struct Info; impl Command for Info { fn name(&self) -> &'static str { "INFO" } fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult { for line in [ format!("echoircd-{VERSION} — a from-scratch IRC daemon in Rust"), "Memory-safe by construction; no unsafe code".to_string(), format!("Running the {} network", s.network), ] { s.numeric(uid, RPL_INFO, &format!(":{line}")); } s.numeric(uid, RPL_ENDOFINFO, ":End of /INFO list"); CmdResult::Ok } } struct Stats; impl Command for Stats { fn name(&self) -> &'static str { "STATS" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let letter = params[0].chars().next().unwrap_or(' '); // everything but uptime exposes server internals — opers only if letter != 'u' && !s.is_oper(uid) { s.numeric( uid, ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator", ); s.numeric( uid, RPL_ENDOFSTATS, &format!("{letter} :End of /STATS report"), ); return CmdResult::Fail; } match letter { 'u' => { let up = now().saturating_sub(s.created); let (d, h, m, sec) = (up / 86400, (up % 86400) / 3600, (up % 3600) / 60, up % 60); s.numeric( uid, RPL_STATSUPTIME, &format!(":Server Up {d} days {h:02}:{m:02}:{sec:02}"), ); } 'o' => { let opers: Vec = s.opers.iter().map(|o| o.name.clone()).collect(); for n in opers { s.numeric(uid, RPL_STATSOLINE, &format!("O * * {n} :oper")); } } 'k' | 'g' | 'z' | 'e' | 'q' | 's' | 'S' | 'R' => { let kind = match letter { 'k' => XKind::Kline, 'g' => XKind::Gline, 'z' => XKind::Zline, 'e' => XKind::Eline, 'q' => XKind::Qline, 'S' => XKind::Svshold, 'R' => XKind::Rline, _ => XKind::Shun, }; let rows: Vec = s .xlines .iter() .filter(|x| x.kind == kind) .map(|x| { format!( "{} {} {} {} :{}", x.kind.tag(), x.mask, x.expires, x.setter, x.reason ) }) .collect(); for r in rows { s.numeric(uid, RPL_STATSXLINE, &r); } } 'l' => { for sv in s.servers.values() { s.numeric( uid, RPL_STATSLINKINFO, &format!("{} 0 0 0 0 :{}", sv.name, sv.desc), ); } } _ => {} } s.numeric( uid, RPL_ENDOFSTATS, &format!("{letter} :End of /STATS report"), ); CmdResult::Ok } } struct Map; impl Command for Map { fn name(&self) -> &'static str { "MAP" } fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult { s.numeric( uid, RPL_MAP, &format!("{} ({} users)", s.name, s.users.len()), ); // hideservices: services (U-lined) servers are hidden from non-opers. let hide_svc = s.conf_bool("hideservices", false) && !s.is_oper(uid); let mut peers: Vec = s .servers .values() .filter(|sv| !(hide_svc && sv.is_service)) .map(|sv| sv.name.clone()) .collect(); peers.sort(); for name in peers { s.numeric(uid, RPL_MAP, &format!("`- {name}")); } s.numeric(uid, RPL_MAPEND, ":End of /MAP"); CmdResult::Ok } }