services: all pseudo-clients join a configurable channel (default #services)
All checks were successful
CI / check (push) Successful in 3m39s

This commit is contained in:
Jean Chevronnet 2026-07-17 01:23:18 +00:00
parent 657154e268
commit f2774f13fe
No known key found for this signature in database
3 changed files with 25 additions and 3 deletions

View file

@ -289,6 +289,14 @@ pub struct Server {
// "is a <this>". Default "Network Service". Empty leaves them non-opers. // "is a <this>". Default "Network Service". Empty leaves them non-opers.
#[serde(default = "default_service_oper_type")] #[serde(default = "default_service_oper_type")]
pub service_oper_type: String, 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 { fn default_service_modes() -> String {

View file

@ -105,6 +105,7 @@ pub struct Engine {
guest_nick: String, // base for the Guest#### rename on enforcement guest_nick: String, // base for the Guest#### rename on enforcement
service_host: String, // hostname the service pseudo-clients wear service_host: String, // hostname the service pseudo-clients wear
service_oper_type: String, // oper type our services are flagged with (WHOIS "is a <this>"); empty = don't oper service_oper_type: String, // oper type our services are flagged with (WHOIS "is a <this>"); 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 enforce_seq: u32, // counter appended to guest_nick
pending_enforce: Vec<PendingEnforce>, // registered nicks awaiting identify-or-rename pending_enforce: Vec<PendingEnforce>, // registered nicks awaiting identify-or-rename
bot_uids: HashMap<String, String>, // casefolded bot nick -> live uid (per connection) bot_uids: HashMap<String, String>, // casefolded bot nick -> live uid (per connection)
@ -240,6 +241,7 @@ impl Engine {
guest_nick: "Guest".to_string(), guest_nick: "Guest".to_string(),
service_host: "services.local".to_string(), service_host: "services.local".to_string(),
service_oper_type: String::new(), service_oper_type: String::new(),
services_channel: String::new(),
enforce_seq: 0, enforce_seq: 0,
pending_enforce: Vec::new(), pending_enforce: Vec::new(),
bot_uids: HashMap::new(), bot_uids: HashMap::new(),
@ -778,6 +780,11 @@ impl Engine {
self.service_oper_type = oper_type.into(); 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<String>) {
self.services_channel = channel.into();
}
// A user arrived on (or changed to) a registered nick: if they aren't logged // 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 // 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 // `synced` so a netburst can't enforce every already-online user; a user who
@ -940,16 +947,22 @@ impl Engine {
if !self.service_oper_type.is_empty() { if !self.service_oper_type.is_empty() {
out.push(NetAction::OperType { uid: svc.uid().to_string(), oper_type: self.service_oper_type.clone() }); 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 // Advertise our SASL mechanisms so the uplink can offer `sasl=PLAIN` to
// clients in CAP LS (IRCv3 SASL 3.2). // clients in CAP LS (IRCv3 SASL 3.2).
// Introduce the registered bots too. // Introduce the registered bots too.
out.extend(self.reconcile_bots()); out.extend(self.reconcile_bots());
// DebugServ joins the log channel so its live feed lands there — it must be // DebugServ must be a member of its feed channel to speak there — unless it
// a member to speak, unlike the server-sourced audit notice. // already joined it above as the services channel.
if let (false, Some(chan)) = (self.debugserv_uid.is_empty(), self.log_channel.clone()) { if let (false, Some(chan)) = (self.debugserv_uid.is_empty(), self.log_channel.clone()) {
if !chan.eq_ignore_ascii_case(&self.services_channel) {
out.push(NetAction::ServiceJoin { uid: self.debugserv_uid.clone(), channel: chan }); out.push(NetAction::ServiceJoin { uid: self.debugserv_uid.clone(), channel: chan });
} }
}
// Re-assert the network bans over the fresh link, each with its remaining // 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). // duration so the ircd expires it at the right time (permanent = 0).
let now = self.now_secs(); let now = self.now_secs();

View file

@ -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 }; 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_host(service_host);
engine.lock().await.set_service_oper_type(cfg.server.service_oper_type.clone()); 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())); engine.lock().await.set_log_channel(cfg.log.as_ref().map(|l| l.channel.clone()));
if enabled("debugserv") { if enabled("debugserv") {
// DebugServ speaks the live feed into the log channel; give the engine its uid. // DebugServ speaks the live feed into the log channel; give the engine its uid.