From e0d849b4c9897f4fead9b9c566dd86c1e4fcdace Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 23:00:07 +0000 Subject: [PATCH] showfile: serve a text file as its own command (e.g. /RULES), config-driven like aliases --- echoircd.conf.example | 4 ++++ src/ircd.rs | 5 +++++ src/modules/mod.rs | 1 + src/modules/showfile.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+) create mode 100644 src/modules/showfile.rs diff --git a/echoircd.conf.example b/echoircd.conf.example index 9faaa68..230fdc6 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -117,6 +117,10 @@ amu_target = both # hidewhois_hide_server = yes # hide 312 # hidewhois_hide_idle = yes # hide 317 # hidewhois_hide_secure = yes # hide 671 +# --- showfile (m_showfile): serve a text file as its own command. One line per +# file: `showfile = `. The file is read fresh each use, so +# edits show without a rehash. e.g. make /RULES stream a rules file: +# showfile = RULES /etc/echoircd/rules.txt # --- geoip (m_geo_maxmind): native MaxMind .mmdb country lookup. Enables the # G: ban extban (e.g. +b G:CN,RU), the oper GEOIP command and # a country line in WHOIS (opers). Point at a GeoLite2-Country.mmdb file: diff --git a/src/ircd.rs b/src/ircd.rs index 5fd1dac..05645cd 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -283,6 +283,11 @@ impl Ircd { let Some(handler) = self.commands.get(cmd) else { if registered { + // showfile (m_showfile): config `showfile = ` streams a + // text file as its own command (e.g. /RULES), like a config-named alias. + if crate::modules::showfile::maybe_show(&mut self.server, uid, cmd) { + return; + } // command aliases (m_alias): config `alias = ` // e.g. `alias = NS NickServ` makes `/NS help` -> PRIVMSG NickServ :help if let Some(target) = self.server.conf_all("alias").iter().find_map(|line| { diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 4dac687..7675066 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -48,6 +48,7 @@ pub mod rpc; pub mod securelist; pub mod securitygroups; pub mod serverban; +pub mod showfile; pub mod snoop; pub mod tline; pub mod whoisport; diff --git a/src/modules/showfile.rs b/src/modules/showfile.rs new file mode 100644 index 0000000..08bc44a --- /dev/null +++ b/src/modules/showfile.rs @@ -0,0 +1,41 @@ +//! showfile — serve a text file as its own command (InspIRCd's `m_showfile`). +//! Config, one line per file: +//! +//! ```text +//! showfile = # e.g. showfile = RULES /etc/echoircd/rules.txt +//! ``` +//! +//! makes `/RULES` stream the file to the client. Dispatched from the same place as +//! command aliases (an unknown, config-named command), so no static registration is +//! needed. The file is read fresh on each use, so edits show without a REHASH. +//! Original native Rust. + +use crate::server::Server; +use crate::Uid; + +/// If `cmd` names a configured `showfile`, stream that file to `uid` (one NOTICE +/// per line) and return `true`. Returns `false` if no showfile matches `cmd`, so +/// the caller can fall through to the normal unknown-command handling. +pub fn maybe_show(s: &mut Server, uid: Uid, cmd: &str) -> bool { + let path = s.conf_all("showfile").iter().find_map(|line| { + let (name, path) = line.split_once(char::is_whitespace)?; + name.eq_ignore_ascii_case(cmd) + .then(|| path.trim().to_string()) + }); + let Some(path) = path else { + return false; + }; + let nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default(); + match std::fs::read_to_string(&path) { + Ok(body) => { + for line in body.lines() { + s.send(uid, format!(":{} NOTICE {nick} :{line}", s.name)); + } + } + Err(_) => s.send( + uid, + format!(":{} NOTICE {nick} :*** {cmd}: file not available.", s.name), + ), + } + true +}