From 835c1cf099e93593430b3cd9532e596aeabcea9d Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 00:54:56 +0000 Subject: [PATCH] user modes +W showwhois / +c commonchans --- src/coremods/core_info.rs | 10 ++++++++++ src/coremods/core_message.rs | 28 ++++++++++++++++++++++++++++ src/mode.rs | 18 +++++++++++++++++- src/numeric.rs | 1 + src/users.rs | 10 +++++++++- 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/coremods/core_info.rs b/src/coremods/core_info.rs index dfbfea5..5f1d1e1 100644 --- a/src/coremods/core_info.rs +++ b/src/coremods/core_info.rs @@ -119,6 +119,7 @@ impl Command for Whois { signon, certfp, swhois, + showwhois, ) = { let u = &s.users[&tuid]; ( @@ -140,6 +141,7 @@ impl Command for Whois { u.ext .get::() .map(|w| w.0.clone()), + u.flags.showwhois, ) }; let chans: Vec = keys @@ -221,6 +223,14 @@ impl Command for Whois { RPL_WHOISIDLE, &format!("{nick} {idle} {signon} :seconds idle, signon time"), ); + // +W showwhois — tell the target that someone looked them up + if showwhois && !is_self { + let by = s.users.get(&uid).map(|u| u.prefix()).unwrap_or_default(); + s.send( + tuid, + format!(":{} NOTICE {nick} :*** {by} did a /WHOIS on you", s.name), + ); + } s.numeric(uid, RPL_ENDOFWHOIS, &format!("{nick} :End of /WHOIS list")); CmdResult::Ok } diff --git a/src/coremods/core_message.rs b/src/coremods/core_message.rs index c392995..8ba226e 100644 --- a/src/coremods/core_message.rs +++ b/src/coremods/core_message.rs @@ -360,6 +360,34 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) // propagate to linked servers that have members in this channel s.send_channel_to_links(uid, &key, target, cmd, &body); } else if let Some(tuid) = s.find_nick(target) { + // user +c (commonchans): only users sharing a channel may PM them (opers exempt) + if s.users + .get(&tuid) + .map(|u| u.flags.deny_uncommon) + .unwrap_or(false) + && uid != tuid + && !s.is_oper(uid) + { + let common = match (s.users.get(&uid), s.users.get(&tuid)) { + (Some(a), Some(b)) => a.channels.intersection(&b.channels).next().is_some(), + _ => false, + }; + if !common { + if !notice { + let tn = s + .users + .get(&tuid) + .map(|u| u.nick.clone()) + .unwrap_or_default(); + s.numeric( + uid, + ERR_CANTSENDTOUSER, + &format!("{tn} :You must share a channel to message this user (+c)"), + ); + } + return CmdResult::Fail; + } + } // user +R (regdeaf): drop messages from users not logged into an account if s.users .get(&tuid) diff --git a/src/mode.rs b/src/mode.rs index 18a4a64..d353fbf 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -931,6 +931,8 @@ static USER_MODES: &[&(dyn UserMode + Sync)] = &[ &SSLPM, &SNOMASK, &CALLERID, + &SHOWWHOIS, + &COMMONCHANS, ]; /// A simple boolean user flag (+i / +w). @@ -973,6 +975,12 @@ fn set_sslpm(f: &mut UserFlags, v: bool) { fn set_callerid(f: &mut UserFlags, v: bool) { f.callerid = v; } +fn set_showwhois(f: &mut UserFlags, v: bool) { + f.showwhois = v; +} +fn set_commonchans(f: &mut UserFlags, v: bool) { + f.deny_uncommon = v; +} static BOT: UFlag = UFlag { ch: 'B', set: set_bot, @@ -1001,6 +1009,14 @@ static CALLERID: UFlag = UFlag { ch: 'g', set: set_callerid, }; +static SHOWWHOIS: UFlag = UFlag { + ch: 'W', + set: set_showwhois, +}; +static COMMONCHANS: UFlag = UFlag { + ch: 'c', + set: set_commonchans, +}; impl UserMode for UFlag { fn letter(&self) -> char { @@ -1141,7 +1157,7 @@ mod tests { #[test] fn registry_covers_all_user_modes() { - for c in "iwoxBDIHrRzsg".chars() { + for c in "iwoxBDIHrRzsgWc".chars() { assert!(user_mode(c).is_some(), "missing umode +{c}"); } assert!(user_mode('Q').is_none()); diff --git a/src/numeric.rs b/src/numeric.rs index 9df5d0c..28d77e5 100644 --- a/src/numeric.rs +++ b/src/numeric.rs @@ -30,6 +30,7 @@ pub const ERR_WASNOSUCHNICK: u16 = 406; pub const ERR_UNAVAILRESOURCE: u16 = 437; // channel temporarily unavailable (+j) pub const ERR_LINKCHANNEL: u16 = 470; // +L — you were redirected to another channel pub const ERR_DELAYREJOIN: u16 = 495; // +J — must wait before rejoining after a kick +pub const ERR_CANTSENDTOUSER: u16 = 531; // +c — no shared channel with the target pub const RPL_ENDOFSPAMFILTER: u16 = 940; // end of the +g word-filter list pub const RPL_SPAMFILTER: u16 = 941; // one +g word-filter entry pub const RPL_KNOCK: u16 = 710; // channel gets the knock diff --git a/src/users.rs b/src/users.rs index 527d611..1699869 100644 --- a/src/users.rs +++ b/src/users.rs @@ -30,6 +30,8 @@ pub struct UserFlags { pub ssl_pm: bool, // +z (only accept PMs from TLS users) pub snomask: bool, // +s (oper: receive server notices) pub callerid: bool, // +g (only accept PMs from users on the ACCEPT list) + pub showwhois: bool, // +W (get a notice when someone WHOISes you) + pub deny_uncommon: bool, // +c (only users sharing a channel may PM you) pub away: Option, // AWAY message, if set } @@ -75,6 +77,12 @@ impl UserFlags { if self.callerid { s.push('g'); } + if self.showwhois { + s.push('W'); + } + if self.deny_uncommon { + s.push('c'); + } s } } @@ -409,7 +417,7 @@ impl Server { uid, RPL_MYINFO, &format!( - "{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJ", + "{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJ", self.name ), );