diff --git a/api/src/lib.rs b/api/src/lib.rs index 52fba51..7522269 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -73,6 +73,9 @@ pub enum NetAction { // Answer a remote IDLE request for one of our pseudo-clients so a routed // WHOIS completes: `: IDLE `. IdleReply { target: String, requester: String, signon: u64, idle: u64 }, + // Flag one of our pseudo-clients as a services operator so WHOIS labels it a + // network service (the oper line): `: OPERTYPE :`. + OperType { uid: String, oper_type: String }, IntroduceUser { uid: String, nick: String, ident: String, host: String, gecos: String }, Privmsg { from: String, to: String, text: String }, Notice { from: String, to: String, text: String }, diff --git a/modules/protocol/inspircd/src/lib.rs b/modules/protocol/inspircd/src/lib.rs index 062f586..e79f232 100644 --- a/modules/protocol/inspircd/src/lib.rs +++ b/modules/protocol/inspircd/src/lib.rs @@ -298,6 +298,11 @@ impl Protocol for InspIrcd { NetAction::IdleReply { target, requester, signon, idle } => { vec![format!(":{target} IDLE {requester} {signon} {idle}")] } + // Oper up a pseudo-client (legacy form, no privilege tags = full access; + // the type name is what WHOIS shows: "is a "). + NetAction::OperType { uid, oper_type } => { + vec![format!(":{uid} OPERTYPE :{oper_type}")] + } // insp4 UID: uuid nickts nick realhost disphost realuser dispuser ip signonts +modes :gecos // (both a real AND a displayed user field — see m_spanningtree/uid.cpp Builder). NetAction::IntroduceUser { uid, nick, ident, host, gecos } => vec![self.sourced(format!( diff --git a/src/config.rs b/src/config.rs index 7fb895d..9eed02e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -285,10 +285,18 @@ pub struct Server { // services server U-lined), bot, block-CTCP. Set per the ircd's loaded modules. #[serde(default = "default_service_modes")] pub service_modes: String, + // Oper type the service pseudo-clients are flagged with, so WHOIS shows + // "is a ". Default "Network Service". Empty leaves them non-opers. + #[serde(default = "default_service_oper_type")] + pub service_oper_type: String, } fn default_service_modes() -> String { - "iHkBT".to_string() + "ikBT".to_string() +} + +fn default_service_oper_type() -> String { + "Network Service".to_string() } fn default_protocol() -> u32 { diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 3d323e2..74de2f8 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -104,6 +104,7 @@ pub struct Engine { synced: bool, 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 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) @@ -238,6 +239,7 @@ impl Engine { synced: false, guest_nick: "Guest".to_string(), service_host: "services.local".to_string(), + service_oper_type: String::new(), enforce_seq: 0, pending_enforce: Vec::new(), bot_uids: HashMap::new(), @@ -771,6 +773,11 @@ impl Engine { } } + // Oper type our services are flagged with at introduction (empty = don't oper). + pub fn set_service_oper_type(&mut self, oper_type: impl Into) { + self.service_oper_type = oper_type.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 @@ -929,6 +936,10 @@ impl Engine { host: self.service_host.clone(), gecos: svc.gecos().to_string(), }); + // Oper them up so WHOIS labels them a network service (the oper line). + if !self.service_oper_type.is_empty() { + out.push(NetAction::OperType { uid: svc.uid().to_string(), oper_type: self.service_oper_type.clone() }); + } } // Advertise our SASL mechanisms so the uplink can offer `sasl=PLAIN` to // clients in CAP LS (IRCv3 SASL 3.2). diff --git a/src/main.rs b/src/main.rs index f96134e..7319c4c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -189,6 +189,7 @@ async fn main() -> Result<()> { // Service pseudo-clients wear the configured host, or the server name. 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_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.