diff --git a/echoircd.conf.example b/echoircd.conf.example index c3c9e1c..66ba9d1 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -70,3 +70,13 @@ amu_target = both # +G censor words: `badword = [replacement]` (omit replacement to block). # badword = examplebadword *** + +# --- OPERMOTD: message shown to opers via /OPERMOTD (one line per entry) --- +# opermotd = Welcome to the staff team. + +# --- self-service vhosts: /VHOST sets your displayed host --- +# vhost = alice s3cret alice.staff.example + +# --- command aliases: /NS ... -> PRIVMSG :... (services shortcuts) --- +# alias = NS NickServ +# alias = CS ChanServ diff --git a/src/config.rs b/src/config.rs index 9081571..fc82dd8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -76,6 +76,9 @@ pub struct Config { pub dnsbl_reason: String, // ban reason for a DNSBL hit pub sasl_server: String, // linked services server that handles SASL ("" = none) pub webirc: Vec<(String, String, String)>, // web gateways: (password, name, ip-mask) + pub opermotd: Vec, // OPERMOTD text, one line per entry + pub vhosts: Vec<(String, String, String)>, // self-service vhosts: (user, pass, host) + pub aliases: Vec<(String, String)>, // command aliases: (name, target-nick) } impl Default for Config { @@ -104,6 +107,9 @@ impl Default for Config { dnsbl_reason: "Your host is listed in a DNS blocklist".to_string(), sasl_server: String::new(), webirc: Vec::new(), + opermotd: Vec::new(), + vhosts: Vec::new(), + aliases: Vec::new(), } } } @@ -246,6 +252,22 @@ impl Config { c.webirc.push((pass.to_string(), gw, mask)); } } + "opermotd" => c.opermotd.push(v.to_string()), + "vhost" => { + // vhost = + let mut it = v.split_whitespace(); + if let (Some(u), Some(p), Some(h)) = (it.next(), it.next(), it.next()) { + c.vhosts.push((u.to_string(), p.to_string(), h.to_string())); + } + } + "alias" => { + // alias = (e.g. `alias = NS NickServ`) + let mut it = v.split_whitespace(); + if let (Some(name), Some(target)) = (it.next(), it.next()) { + c.aliases + .push((name.to_ascii_uppercase(), target.to_string())); + } + } _ => {} } } diff --git a/src/coremods/core_oper.rs b/src/coremods/core_oper.rs index c6e9f01..294ea70 100644 --- a/src/coremods/core_oper.rs +++ b/src/coremods/core_oper.rs @@ -52,9 +52,47 @@ pub fn commands() -> Vec> { Box::new(SetIdle), Box::new(NickLock), Box::new(NickUnlock), + Box::new(OperMotd), ] } +/// OPERMOTD — show the IRC-operators' message of the day (InspIRCd `m_opermotd`), +/// 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); + if s.opermotd.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 s.opermotd.clone() { + 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). InspIRCd `m_swhois`. pub struct Swhois(pub String); diff --git a/src/coremods/core_user.rs b/src/coremods/core_user.rs index c45b12c..e55c96f 100644 --- a/src/coremods/core_user.rs +++ b/src/coremods/core_user.rs @@ -22,9 +22,52 @@ pub fn commands() -> Vec> { Box::new(Away), Box::new(SetName), Box::new(WebIrc), + Box::new(Vhost), ] } +/// VHOST — claim a self-service virtual host with `VHOST ` matching a +/// configured `vhost = ` block (InspIRCd `m_vhost`). +struct Vhost; +impl Command for Vhost { + fn name(&self) -> &'static str { + "VHOST" + } + fn min_params(&self) -> usize { + 2 + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + let (user, pass) = (¶ms[0], ¶ms[1]); + let host = s + .vhosts + .iter() + .find(|(u, p, _)| u == user && p == pass) + .map(|(_, _, h)| h.clone()); + let nick = s + .users + .get(&uid) + .map(|u| u.nick.clone()) + .unwrap_or_else(|| "*".to_string()); + match host { + Some(h) => { + s.change_host_ident(uid, None, Some(&h)); + s.send( + uid, + format!(":{} NOTICE {nick} :Your vhost is now {h}", s.name), + ); + } + None => { + s.send( + uid, + format!(":{} NOTICE {nick} :Invalid vhost credentials", s.name), + ); + return CmdResult::Fail; + } + } + CmdResult::Ok + } +} + /// WEBIRC — a trusted web gateway declares the real client's host + IP, so users /// behind it don't all share the gateway's address. `WEBIRC /// [:flags]`; must precede registration and the password must diff --git a/src/ircd.rs b/src/ircd.rs index 5919042..86ddb91 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -228,6 +228,25 @@ impl Ircd { let Some(handler) = self.commands.get(cmd) else { if registered { + // command aliases (m_alias): `/NS help` -> PRIVMSG NickServ :help + if let Some(target) = self + .server + .aliases + .iter() + .find(|(n, _)| n == cmd) + .map(|(_, t)| t.clone()) + { + if !msg.params.is_empty() { + let text = msg.params.join(" "); + crate::coremods::core_message::deliver( + &mut self.server, + uid, + &[target, text], + false, + ); + } + return; + } self.server .numeric(uid, ERR_UNKNOWNCOMMAND, &format!("{cmd} :Unknown command")); } diff --git a/src/server.rs b/src/server.rs index bf396f4..00fcd3c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -132,6 +132,9 @@ pub struct Server { pub dnsbl_reason: String, // ban reason on a DNSBL hit pub sasl_server: String, // services server that handles SASL pub webirc: Vec<(String, String, String)>, // web gateways: (password, name, ip-mask) + pub opermotd: Vec, // OPERMOTD text + pub vhosts: Vec<(String, String, String)>, // self-service vhosts: (user, pass, host) + pub aliases: Vec<(String, String)>, // command aliases: (name, target-nick) // labeled-response: while Some((uid, buf)), that client's own responses are // diverted into `buf` instead of the socket, so `on_line` can wrap them with // the command's `label` (single tag, BATCH, or ACK). RefCell because the @@ -183,6 +186,9 @@ impl Server { dnsbl_reason: cfg.dnsbl_reason, sasl_server: cfg.sasl_server, webirc: cfg.webirc, + opermotd: cfg.opermotd, + vhosts: cfg.vhosts, + aliases: cfg.aliases, label_capture: RefCell::new(None), event_tx, conn_counter,