From 68fdd91eaa5e1e1cff8ffd2db5807b4966c621e0 Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 00:29:54 +0000 Subject: [PATCH] SWHOIS (320), SETIDLE, UNINVITE --- src/coremods/core_channel.rs | 71 ++++++++++++++++++++++++++++++++++++ src/coremods/core_info.rs | 8 ++++ src/coremods/core_oper.rs | 60 ++++++++++++++++++++++++++++++ src/numeric.rs | 1 + 4 files changed, 140 insertions(+) diff --git a/src/coremods/core_channel.rs b/src/coremods/core_channel.rs index 1479245..0936545 100644 --- a/src/coremods/core_channel.rs +++ b/src/coremods/core_channel.rs @@ -15,6 +15,7 @@ pub fn commands() -> Vec> { Box::new(TopicCmd), Box::new(Names), Box::new(Invite), + Box::new(Uninvite), Box::new(Knock), Box::new(Cycle), Box::new(Remove), @@ -243,6 +244,76 @@ impl Command for Invite { } } +/// UNINVITE — revoke a pending invite (InspIRCd `m_uninvite`). `UNINVITE +/// <#chan>`; a channel op cancels an invite they (or another op) issued. +struct Uninvite; +impl Command for Uninvite { + fn name(&self) -> &'static str { + "UNINVITE" + } + fn min_params(&self) -> usize { + 2 + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + let (tnick, chan) = (¶ms[0], ¶ms[1]); + let key = chan.to_ascii_lowercase(); + if !s.channels.contains_key(&key) { + s.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{chan} :No such channel")); + return CmdResult::Fail; + } + if !s.is_member(uid, &key) { + s.numeric( + uid, + ERR_NOTONCHANNEL, + &format!("{chan} :You're not on that channel"), + ); + return CmdResult::Fail; + } + if !s.is_op(uid, &key) { + s.numeric( + uid, + ERR_CHANOPRIVSNEEDED, + &format!("{chan} :You're not a channel operator"), + ); + return CmdResult::Fail; + } + let Some(tuid) = s.find_nick(tnick) else { + s.numeric( + uid, + ERR_NOSUCHNICK, + &format!("{tnick} :No such nick/channel"), + ); + return CmdResult::Fail; + }; + let removed = s + .channels + .get_mut(&key) + .map(|ch| ch.invites.remove(&tuid)) + .unwrap_or(false); + let who = s.users[&tuid].nick.clone(); + let word = if removed { + "is no longer invited to" + } else { + "was not invited to" + }; + let nick = s.users[&uid].nick.clone(); + s.send( + uid, + format!(":{} NOTICE {nick} :{who} {word} {chan}", s.name), + ); + if removed { + s.send( + tuid, + format!( + ":{} NOTICE {who} :Your invite to {chan} was revoked", + s.name + ), + ); + } + CmdResult::Ok + } +} + struct Join; impl Command for Join { fn name(&self) -> &'static str { diff --git a/src/coremods/core_info.rs b/src/coremods/core_info.rs index 5d54ac7..81e3c4d 100644 --- a/src/coremods/core_info.rs +++ b/src/coremods/core_info.rs @@ -118,6 +118,7 @@ impl Command for Whois { last_active, signon, certfp, + swhois, ) = { let u = &s.users[&tuid]; ( @@ -136,6 +137,9 @@ impl Command for Whois { u.last_active, u.signon, u.certfp.clone(), + u.ext + .get::() + .map(|w| w.0.clone()), ) }; let chans: Vec = keys @@ -171,6 +175,10 @@ impl Command for Whois { &format!("{nick} :is an IRC operator"), ); } + // 320: oper-set SWHOIS line + if let Some(line) = &swhois { + s.numeric(uid, RPL_WHOISSPECIAL, &format!("{nick} :{line}")); + } // opers can see through the cloak to the real host/ip if asker_oper && disp != realhost { s.numeric( diff --git a/src/coremods/core_oper.rs b/src/coremods/core_oper.rs index 33adba6..dbb9549 100644 --- a/src/coremods/core_oper.rs +++ b/src/coremods/core_oper.rs @@ -47,9 +47,15 @@ pub fn commands() -> Vec> { Box::new(ClearChan), Box::new(Check), Box::new(AllTime), + Box::new(SwhoisCmd), + Box::new(SetIdle), ] } +/// An oper-set WHOIS line, stored per-user in `User.ext` and rendered by WHOIS +/// (RPL_WHOISSPECIAL 320). InspIRCd `m_swhois`. +pub struct Swhois(pub String); + /// Reject non-opers with 481; returns whether the caller is an oper. fn require_oper(s: &mut Server, uid: Uid) -> bool { if s.is_oper(uid) { @@ -1185,6 +1191,60 @@ impl Command for Check { } } +/// SWHOIS — attach (or clear) an extra WHOIS line on a user (InspIRCd `m_swhois`). +/// `SWHOIS :`; an empty text removes it. Shown as RPL_WHOISSPECIAL. +struct SwhoisCmd; +impl Command for SwhoisCmd { + fn name(&self) -> &'static str { + "SWHOIS" + } + fn min_params(&self) -> usize { + 2 + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + if !require_oper(s, uid) { + return CmdResult::Fail; + } + let Some(t) = oper_target(s, uid, ¶ms[0]) else { + return CmdResult::Fail; + }; + let text = params[1].clone(); + if let Some(u) = s.users.get_mut(&t) { + if text.is_empty() { + u.ext.take::(); + } else { + u.ext.set(Swhois(text.clone())); + } + } + let by = oper_nick(s, uid); + s.snotice(&format!("{by} used SWHOIS on {}: {text}", params[0])); + CmdResult::Ok + } +} + +/// SETIDLE — reset your own idle time (InspIRCd `m_setidle`). `SETIDLE ` +/// backdates the last-activity clock so WHOIS shows that idle time. +struct SetIdle; +impl Command for SetIdle { + fn name(&self) -> &'static str { + "SETIDLE" + } + fn min_params(&self) -> usize { + 1 + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + if !require_oper(s, uid) { + return CmdResult::Fail; + } + let secs: u64 = params[0].parse().unwrap_or(0); + if let Some(u) = s.users.get_mut(&uid) { + u.last_active = now().saturating_sub(secs); + } + onotice(s, uid, &format!("*** SETIDLE: idle time set to {secs}s")); + CmdResult::Ok + } +} + /// ALLTIME — show the current server time to the requesting oper (InspIRCd /// `m_alltime`; on a single server there's just the one time to report). struct AllTime; diff --git a/src/numeric.rs b/src/numeric.rs index a03c817..9f0b4b0 100644 --- a/src/numeric.rs +++ b/src/numeric.rs @@ -15,6 +15,7 @@ pub const RPL_ADMINEMAIL: u16 = 259; pub const RPL_USERHOST: u16 = 302; pub const RPL_ISON: u16 = 303; pub const RPL_WHOISIDLE: u16 = 317; +pub const RPL_WHOISSPECIAL: u16 = 320; // SWHOIS oper-set whois line pub const RPL_LISTSTART: u16 = 321; pub const RPL_LIST: u16 = 322; pub const RPL_LISTEND: u16 = 323;