From bda7eecfc240118344d08c3739a7321496e41f58 Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 19:52:52 +0000 Subject: [PATCH] modules: port ircv3_extended_isupport (draft/extended-isupport cap + ISUPPORT command, draft/isupport batch) --- src/modules/extended_isupport.rs | 49 +++++++++++++++++++++++++++++ src/modules/mod.rs | 2 ++ src/server.rs | 53 ++++++++++++++++++++++++++++++++ src/users.rs | 35 ++++++++------------- 4 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 src/modules/extended_isupport.rs diff --git a/src/modules/extended_isupport.rs b/src/modules/extended_isupport.rs new file mode 100644 index 0000000..4abf1db --- /dev/null +++ b/src/modules/extended_isupport.rs @@ -0,0 +1,49 @@ +//! extended_isupport — the `draft/extended-isupport` capability. reverse's own +//! module. Normally ISUPPORT (005) is a one-shot at registration; with this cap a +//! client can send the `ISUPPORT` command any time to re-request the current tokens +//! (handy after a rehash changes them). If the client also has `batch`, the reply +//! is wrapped in a `draft/isupport` BATCH so the multi-line set arrives atomically — +//! the emission itself lives in `Server::send_isupport`, shared with the welcome burst. +//! +//! Behaviour reference: reverse's InspIRCd `m_ircv3_extended_isupport`. Original native Rust. + +use crate::command::{CmdResult, Command}; +use crate::numeric::ERR_UNKNOWNCOMMAND; +use crate::server::Server; +use crate::Uid; + +pub fn commands() -> Vec> { + vec![Box::new(ISupportCmd)] +} + +/// ISUPPORT — re-send the server's ISUPPORT tokens. Requires the +/// `draft/extended-isupport` cap; usable before registration completes. +struct ISupportCmd; +impl Command for ISupportCmd { + fn name(&self) -> &'static str { + "ISUPPORT" + } + fn min_params(&self) -> usize { + 0 + } + fn before_reg(&self) -> bool { + true + } + fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult { + let (has_cap, has_batch) = s + .users + .get(&uid) + .map(|u| (u.caps.ext_isupport, u.caps.batch)) + .unwrap_or((false, false)); + if !has_cap { + s.numeric( + uid, + ERR_UNKNOWNCOMMAND, + "ISUPPORT :You must request the draft/extended-isupport capability to use this command", + ); + return CmdResult::Fail; + } + s.send_isupport(uid, has_batch); + CmdResult::Ok + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 00216c8..960ae54 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -15,6 +15,7 @@ pub mod connectban; pub mod connflood; pub mod denychans; pub mod dnsbl; +pub mod extended_isupport; pub mod extjwt; pub mod filehost; pub mod filter; @@ -97,5 +98,6 @@ pub fn module_commands() -> Vec> { .chain(cloudflare_challenge::commands()) .chain(extjwt::commands()) .chain(filehost::commands()) + .chain(extended_isupport::commands()) .collect() } diff --git a/src/server.rs b/src/server.rs index 334ed9b..bec0908 100644 --- a/src/server.rs +++ b/src/server.rs @@ -579,6 +579,59 @@ impl Server { /// Send a numeric: `:server NNN `. `` is the client's /// nick, or `*` before it has one. + /// The ISUPPORT (005) token blocks this server advertises — the fixed set plus + /// the config-driven module tokens (ICON, FILEHOST). Each entry is a token block + /// without the trailing `:are supported by this server`. Shared by the welcome + /// burst and the `ISUPPORT` command (draft/extended-isupport). + pub fn isupport_lines(&self) -> Vec { + let mut lines = vec![format!( + "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgX,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,cgjmnrsy WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={}", + self.network + )]; + if let Some(tok) = crate::modules::network_icon::isupport(self) { + lines.push(tok); + } + if let Some(tok) = crate::modules::filehost::isupport(self) { + lines.push(tok); + } + lines + } + + /// Emit the ISUPPORT numerics to `uid`. When `batched` (the client negotiated + /// `draft/extended-isupport` + `batch`), wrap them in a `draft/isupport` BATCH so + /// the multi-line set arrives atomically (InspIRCd's m_ircv3_extended_isupport). + pub fn send_isupport(&mut self, uid: Uid, batched: bool) { + let lines = self.isupport_lines(); + if batched { + let nick = self + .users + .get(&uid) + .map(|u| u.nick.clone()) + .unwrap_or_default(); + let bref = self.next_msgid().replace('-', ""); + self.send(uid, format!(":{} BATCH +{bref} draft/isupport", self.name)); + for l in &lines { + self.send( + uid, + format!( + "@batch={bref} :{} {:03} {nick} {l} :are supported by this server", + self.name, + crate::numeric::RPL_ISUPPORT + ), + ); + } + self.send(uid, format!(":{} BATCH -{bref}", self.name)); + } else { + for l in &lines { + self.numeric( + uid, + crate::numeric::RPL_ISUPPORT, + &format!("{l} :are supported by this server"), + ); + } + } + } + pub fn numeric(&self, uid: Uid, code: u16, rest: &str) { let target = self .users diff --git a/src/users.rs b/src/users.rs index b67bb59..9630975 100644 --- a/src/users.rs +++ b/src/users.rs @@ -116,6 +116,7 @@ pub const SUPPORTED_CAPS: &[&str] = &[ "draft/multiline", "draft/account-registration", "draft/json-log", + "draft/extended-isupport", "reverse.im/filehost", "cap-notify", ]; @@ -149,6 +150,7 @@ pub struct Caps { pub multiline: bool, // draft/multiline — may send multiline message batches pub acct_registration: bool, // draft/account-registration — REGISTER/VERIFY understood pub json_log: bool, // draft/json-log — structured JSON tag on server notices + pub ext_isupport: bool, // draft/extended-isupport — ISUPPORT command + batched 005 pub filehost: bool, // reverse.im/filehost — knows the file-host extension pub cap_notify: bool, } @@ -210,6 +212,7 @@ impl Caps { "draft/multiline" => self.multiline, "draft/account-registration" => self.acct_registration, "draft/json-log" => self.json_log, + "draft/extended-isupport" => self.ext_isupport, "reverse.im/filehost" => self.filehost, "cap-notify" => self.cap_notify, _ => false, @@ -243,6 +246,7 @@ impl Caps { "draft/multiline" => &mut self.multiline, "draft/account-registration" => &mut self.acct_registration, "draft/json-log" => &mut self.json_log, + "draft/extended-isupport" => &mut self.ext_isupport, "reverse.im/filehost" => &mut self.filehost, "cap-notify" => &mut self.cap_notify, _ => return false, @@ -460,29 +464,14 @@ impl Server { self.name ), ); - self.numeric( - uid, - RPL_ISUPPORT, - &format!( - "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgX,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,cgjmnrsy WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server", - self.network - ), - ); - // ircv3_network_icon: advertise draft/ICON when configured - if let Some(tok) = crate::modules::network_icon::isupport(self) { - self.numeric( - uid, - RPL_ISUPPORT, - &format!("{tok} :are supported by this server"), - ); - } - if let Some(tok) = crate::modules::filehost::isupport(self) { - self.numeric( - uid, - RPL_ISUPPORT, - &format!("{tok} :are supported by this server"), - ); - } + // ISUPPORT (005): the fixed set + config-driven module tokens (ICON/FILEHOST). + // draft/extended-isupport + batch clients get it wrapped in a draft/isupport batch. + let batched = self + .users + .get(&uid) + .map(|u| u.caps.ext_isupport && u.caps.batch) + .unwrap_or(false); + self.send_isupport(uid, batched); self.numeric( uid, RPL_LUSERCLIENT,