From f2774f13fed6a2688ad3a2b42323f6835ce602f1 Mon Sep 17 00:00:00 2001 From: Jean Date: Fri, 17 Jul 2026 01:23:18 +0000 Subject: [PATCH] services: all pseudo-clients join a configurable channel (default #services) --- src/config.rs | 8 ++++++++ src/engine/mod.rs | 19 ++++++++++++++++--- src/main.rs | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 9eed02e..f2c0355 100644 --- a/src/config.rs +++ b/src/config.rs @@ -289,6 +289,14 @@ pub struct Server { // "is a ". Default "Network Service". Empty leaves them non-opers. #[serde(default = "default_service_oper_type")] pub service_oper_type: String, + // Channel every service pseudo-client (NickServ, ChanServ, …) joins at + // startup. Default "#services"; empty leaves them out of any channel. + #[serde(default = "default_services_channel")] + pub services_channel: String, +} + +fn default_services_channel() -> String { + "#services".to_string() } fn default_service_modes() -> String { diff --git a/src/engine/mod.rs b/src/engine/mod.rs index ed0f268..3a54418 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -105,6 +105,7 @@ pub struct Engine { guest_nick: String, // base for the Guest#### rename on enforcement service_host: String, // hostname the service pseudo-clients wear service_oper_type: String, // oper type our services are flagged with (WHOIS "is a "); empty = don't oper + services_channel: String, // channel all service pseudo-clients join at startup; empty = none enforce_seq: u32, // counter appended to guest_nick pending_enforce: Vec, // registered nicks awaiting identify-or-rename bot_uids: HashMap, // casefolded bot nick -> live uid (per connection) @@ -240,6 +241,7 @@ impl Engine { guest_nick: "Guest".to_string(), service_host: "services.local".to_string(), service_oper_type: String::new(), + services_channel: String::new(), enforce_seq: 0, pending_enforce: Vec::new(), bot_uids: HashMap::new(), @@ -778,6 +780,11 @@ impl Engine { self.service_oper_type = oper_type.into(); } + // Channel all service pseudo-clients join at startup (empty = none). + pub fn set_services_channel(&mut self, channel: impl Into) { + self.services_channel = channel.into(); + } + // A user arrived on (or changed to) a registered nick: if they aren't logged // into that account, prompt them to IDENTIFY and schedule a rename. Gated on // `synced` so a netburst can't enforce every already-online user; a user who @@ -940,15 +947,21 @@ impl Engine { if !self.service_oper_type.is_empty() { out.push(NetAction::OperType { uid: svc.uid().to_string(), oper_type: self.service_oper_type.clone() }); } + // All service pseudo-clients sit in the services channel (configurable). + if !self.services_channel.is_empty() { + out.push(NetAction::ServiceJoin { uid: svc.uid().to_string(), channel: self.services_channel.clone() }); + } } // Advertise our SASL mechanisms so the uplink can offer `sasl=PLAIN` to // clients in CAP LS (IRCv3 SASL 3.2). // Introduce the registered bots too. out.extend(self.reconcile_bots()); - // DebugServ joins the log channel so its live feed lands there — it must be - // a member to speak, unlike the server-sourced audit notice. + // DebugServ must be a member of its feed channel to speak there — unless it + // already joined it above as the services channel. if let (false, Some(chan)) = (self.debugserv_uid.is_empty(), self.log_channel.clone()) { - out.push(NetAction::ServiceJoin { uid: self.debugserv_uid.clone(), channel: chan }); + if !chan.eq_ignore_ascii_case(&self.services_channel) { + out.push(NetAction::ServiceJoin { uid: self.debugserv_uid.clone(), channel: chan }); + } } // Re-assert the network bans over the fresh link, each with its remaining // duration so the ircd expires it at the right time (permanent = 0). diff --git a/src/main.rs b/src/main.rs index 7319c4c..2aa2a77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -190,6 +190,7 @@ async fn main() -> Result<()> { let service_host = if cfg.server.service_host.is_empty() { &cfg.server.name } else { &cfg.server.service_host }; engine.lock().await.set_service_host(service_host); engine.lock().await.set_service_oper_type(cfg.server.service_oper_type.clone()); + engine.lock().await.set_services_channel(cfg.server.services_channel.clone()); engine.lock().await.set_log_channel(cfg.log.as_ref().map(|l| l.channel.clone())); if enabled("debugserv") { // DebugServ speaks the live feed into the log channel; give the engine its uid.