From b8e24e207157dcd5da7d89fb8c09a8cad86d3bff Mon Sep 17 00:00:00 2001 From: reverse Date: Fri, 21 Aug 2026 17:11:27 +0000 Subject: [PATCH] chanlog: filter each log channel by snomask category; repeatable so different snomasks route to different channels --- echoircd.conf.example | 9 ++++++-- src/modules/chanlog.rs | 47 ++++++++++++++++++++++++++++-------------- src/server.rs | 26 ++++++++++++++++++++++- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/echoircd.conf.example b/echoircd.conf.example index 125e5d6..61f7f92 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -293,8 +293,13 @@ amu_target = both # hidewhois_hide_idle = yes # hide 317 # hidewhois_hide_secure = yes # hide 671 # --- chanlog: mirror the oper server-notice stream into a channel so -# staff can watch it in a normal window. Set the channel (create/keep it opped): -# chanlog = #snotices +# staff can watch it in a normal window. Set the channel (create/keep it opped). +# Add snomask category letters after the channel to log only those categories +# (x x-lines, d dnsbl, c connects, o oper, q quit, k kill, …); no letters logs +# everything. Repeatable, so different snomasks can go to different channels: +# chanlog = #snotices # everything +# chanlog = #bans xdk # only x-lines, dnsbl hits, and kills +# chanlog = #conns cq # only connects and quits # --- extbanbanlist: no config — adds the matching extban # `b:<#channel>`, so `+b b:#staff` catches everyone banned in #staff (shares a # ban list between channels). diff --git a/src/modules/chanlog.rs b/src/modules/chanlog.rs index 673d201..e53e53e 100644 --- a/src/modules/chanlog.rs +++ b/src/modules/chanlog.rs @@ -1,21 +1,38 @@ //! chanlog — mirror server notices (the `snotice` stream opers see with +s) into a -//! channel, so staff can watch the log in a normal channel window. Off unless -//! `chanlog = #channel` is configured. +//! channel, so staff can watch the log in a normal channel window. Off unless at +//! least one `chanlog = #channel [snomask-letters]` is configured. +//! +//! Each entry may carry a snomask filter — the category letters (the same letters +//! as the `+s` snomask set: `x` x-lines, `d` dnsbl, `c` connects, `o` oper, …) to +//! send there. With no letters, every category goes to that channel (the original +//! behaviour). The key is repeatable, so different snomasks can be routed to +//! different channels: `chanlog = #xlog x` / `chanlog = #conns cq`. use crate::server::Server; -/// Tee `msg` to the configured chanlog channel (if set and it exists). Called at -/// the tail of `Server::snotice`. Read-only over server state, so it can't loop. -pub fn tee(s: &Server, msg: &str) { - let Some(chan) = s.conf("chanlog") else { - return; - }; - let key = chan.to_ascii_lowercase(); - if !s.channels.contains_key(&key) { - return; // channel not created yet — nothing to log into +/// Tee a snomask-`cat` server notice `msg` to every configured chanlog channel +/// whose filter admits `cat`. Called at the tail of `Server::snotice_c`. Read-only +/// over server state, so it can't loop. +pub fn tee(s: &Server, cat: char, msg: &str) { + for spec in s.conf_all("chanlog") { + let mut parts = spec.split_whitespace(); + let Some(chan) = parts.next() else { + continue; + }; + // Optional snomask filter: only these category letters go here; absent means + // every category (so a bare `chanlog = #channel` logs everything, as before). + if let Some(masks) = parts.next() { + if !masks.contains(cat) { + continue; + } + } + let key = chan.to_ascii_lowercase(); + if !s.channels.contains_key(&key) { + continue; // channel not created yet — nothing to log into + } + // to_channel builds the line once and shares it by Arc across members (and + // adds the server-time tag per recipient) instead of cloning per member. + let line = format!(":{} NOTICE {chan} :{msg}", s.name); + s.to_channel(&key, &line, None); } - // to_channel builds the line once and shares it by Arc across members (and adds - // the server-time tag per recipient) instead of cloning a String per member. - let line = format!(":{} NOTICE {chan} :{msg}", s.name); - s.to_channel(&key, &line, None); } diff --git a/src/server.rs b/src/server.rs index 4ccf4c3..b61685d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1034,7 +1034,7 @@ impl Server { for o in opers { self.deliver_server_notice(o, msg, &jval); } - crate::modules::chanlog::tee(self, msg); + crate::modules::chanlog::tee(self, cat, msg); crate::modules::syslog::tee(self, msg); crate::modules::log_json::tee(self, msg); } @@ -1655,6 +1655,30 @@ mod tests { assert!(joined.contains("XLINE: Z-line on 192.0.2.5 expired"), "expire: {joined}"); } + #[test] + fn chanlog_routes_by_snomask() { + use crate::channels::{Channel, Member}; + let mut s = srv(); + // #xlog takes only x-line (x) notices; #all takes every category + s.raw_config.insert("chanlog".to_string(), vec!["#xlog x".to_string(), "#all".to_string()]); + let rx = add_user(&mut s, 1, "logbot"); + for name in ["#xlog", "#all"] { + let mut c = Channel::new(name); + c.members.insert(1, Member::default()); + s.channels.insert(name.to_string(), c); + } + s.snotice_c('x', "XLINEMSG"); + s.snotice_c('c', "CONNMSG"); + let lines: Vec = std::iter::from_fn(|| rx.try_recv().ok()).collect(); + let logged = |chan: &str, needle: &str| { + lines.iter().any(|l| l.contains(&format!("NOTICE {chan} :")) && l.contains(needle)) + }; + assert!(logged("#xlog", "XLINEMSG"), "x-line notice goes to #xlog: {lines:?}"); + assert!(logged("#all", "XLINEMSG"), "x-line notice goes to #all: {lines:?}"); + assert!(!logged("#xlog", "CONNMSG"), "connect notice filtered out of #xlog: {lines:?}"); + assert!(logged("#all", "CONNMSG"), "connect notice goes to #all: {lines:?}"); + } + #[test] fn banned_user_message_shows_expiry() { let mut s = srv();