//! relaymsg — `RELAYMSG ` (IRCv3 `draft/relaymsg`): an //! operator whose client negotiated the capability, and who is in the channel, //! sends a channel message under a spoofed "relay" nick (e.g. `discord/alice`), for //! stateless bridges (operator-only, as in InspIRCd — the source nick is spoofed). //! The message is //! tagged `@draft/relaymsg=` so clients can attribute it. The spoofed nick //! must contain a configured separator and must not collide with a real nick. //! //! Config: `relaymsg_separators` (default `/`), `relaymsg_ident` (default `relay`), //! `relaymsg_host` (default = server name). Local delivery only; cross-server ENCAP //! relay is not propagated. use crate::command::{CmdResult, Command}; use crate::numeric::{ERR_BADRELAYNICK, ERR_CANNOTSENDTOCHAN, ERR_NOPRIVILEGES, ERR_NOSUCHCHANNEL}; use crate::server::Server; use crate::Uid; /// Characters never allowed in a spoofed relay nick (core IRC syntax). const FORBIDDEN: &str = "!+%@&#$:'\"?*,."; pub fn commands() -> Vec> { vec![Box::new(RelayMsg)] } struct RelayMsg; impl Command for RelayMsg { fn name(&self) -> &'static str { "RELAYMSG" } fn min_params(&self) -> usize { 3 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let (chan, nick, text) = (¶ms[0], ¶ms[1], ¶ms[2]); let bad = |s: &mut Server, msg: &str| { s.numeric(uid, ERR_BADRELAYNICK, &format!("{nick} :{msg}")); CmdResult::Fail }; // RELAYMSG spoofs an arbitrary source nick, so it's operator-only (as in // InspIRCd, CmdAccess::OPERATOR) — bridges run their relay as an oper. if !s.is_oper(uid) { s.numeric( uid, ERR_NOPRIVILEGES, ":Permission Denied- RELAYMSG is for IRC operators", ); return CmdResult::Fail; } if !s.users.get(&uid).map(|u| u.caps.relaymsg).unwrap_or(false) { s.numeric( uid, ERR_NOPRIVILEGES, ":You must enable the draft/relaymsg capability to use RELAYMSG", ); return CmdResult::Fail; } 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; } if !s.is_member(uid, &key) { s.numeric( uid, ERR_CANNOTSENDTOCHAN, &format!("{chan} :You must be in the channel to use RELAYMSG"), ); return CmdResult::Fail; } if s.find_nick(nick).is_some() || s.remote_nick.contains_key(&nick.to_ascii_lowercase()) { return bad(s, "RELAYMSG spoofed nick is already in use"); } if nick.chars().any(|c| FORBIDDEN.contains(c)) { return bad(s, "Invalid characters in spoofed nick"); } let seps = s .conf("relaymsg_separators") .filter(|v| !v.is_empty()) .unwrap_or("/") .to_string(); if !nick.chars().any(|c| seps.contains(c)) { return bad( s, &format!("Spoofed nick must include one of these separators: {seps}"), ); } // build the fake source and relay it to every member (sender included, so // their own client sees the @draft/relaymsg echo) let ident = s.conf("relaymsg_ident").filter(|v| !v.is_empty()).unwrap_or("relay").to_string(); let host = s .conf("relaymsg_host") .filter(|v| !v.is_empty()) .unwrap_or(s.name.as_str()) .to_string(); let sender = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default(); let body = format!(":{nick}!{ident}@{host} PRIVMSG {chan} :{text}"); let ctags = format!("draft/relaymsg={sender}"); let msgid = s.next_msgid(); let members: Vec = s .channels .get(&key) .map(|c| c.members.keys().copied().collect()) .unwrap_or_default(); for m in members { s.send_tagged(m, uid, &ctags, &msgid, &body); } CmdResult::Ok } }