user modes +W showwhois / +c commonchans

This commit is contained in:
Jean Chevronnet 2026-08-09 00:54:56 +00:00
parent 37ea5d277d
commit 835c1cf099
5 changed files with 65 additions and 2 deletions

View file

@ -119,6 +119,7 @@ impl Command for Whois {
signon, signon,
certfp, certfp,
swhois, swhois,
showwhois,
) = { ) = {
let u = &s.users[&tuid]; let u = &s.users[&tuid];
( (
@ -140,6 +141,7 @@ impl Command for Whois {
u.ext u.ext
.get::<crate::coremods::core_oper::Swhois>() .get::<crate::coremods::core_oper::Swhois>()
.map(|w| w.0.clone()), .map(|w| w.0.clone()),
u.flags.showwhois,
) )
}; };
let chans: Vec<String> = keys let chans: Vec<String> = keys
@ -221,6 +223,14 @@ impl Command for Whois {
RPL_WHOISIDLE, RPL_WHOISIDLE,
&format!("{nick} {idle} {signon} :seconds idle, signon time"), &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")); s.numeric(uid, RPL_ENDOFWHOIS, &format!("{nick} :End of /WHOIS list"));
CmdResult::Ok CmdResult::Ok
} }

View file

@ -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 // propagate to linked servers that have members in this channel
s.send_channel_to_links(uid, &key, target, cmd, &body); s.send_channel_to_links(uid, &key, target, cmd, &body);
} else if let Some(tuid) = s.find_nick(target) { } 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 // user +R (regdeaf): drop messages from users not logged into an account
if s.users if s.users
.get(&tuid) .get(&tuid)

View file

@ -931,6 +931,8 @@ static USER_MODES: &[&(dyn UserMode + Sync)] = &[
&SSLPM, &SSLPM,
&SNOMASK, &SNOMASK,
&CALLERID, &CALLERID,
&SHOWWHOIS,
&COMMONCHANS,
]; ];
/// A simple boolean user flag (+i / +w). /// 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) { fn set_callerid(f: &mut UserFlags, v: bool) {
f.callerid = v; 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 { static BOT: UFlag = UFlag {
ch: 'B', ch: 'B',
set: set_bot, set: set_bot,
@ -1001,6 +1009,14 @@ static CALLERID: UFlag = UFlag {
ch: 'g', ch: 'g',
set: set_callerid, 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 { impl UserMode for UFlag {
fn letter(&self) -> char { fn letter(&self) -> char {
@ -1141,7 +1157,7 @@ mod tests {
#[test] #[test]
fn registry_covers_all_user_modes() { 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(c).is_some(), "missing umode +{c}");
} }
assert!(user_mode('Q').is_none()); assert!(user_mode('Q').is_none());

View file

@ -30,6 +30,7 @@ pub const ERR_WASNOSUCHNICK: u16 = 406;
pub const ERR_UNAVAILRESOURCE: u16 = 437; // channel temporarily unavailable (+j) 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_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_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_ENDOFSPAMFILTER: u16 = 940; // end of the +g word-filter list
pub const RPL_SPAMFILTER: u16 = 941; // one +g word-filter entry pub const RPL_SPAMFILTER: u16 = 941; // one +g word-filter entry
pub const RPL_KNOCK: u16 = 710; // channel gets the knock pub const RPL_KNOCK: u16 = 710; // channel gets the knock

View file

@ -30,6 +30,8 @@ pub struct UserFlags {
pub ssl_pm: bool, // +z (only accept PMs from TLS users) pub ssl_pm: bool, // +z (only accept PMs from TLS users)
pub snomask: bool, // +s (oper: receive server notices) pub snomask: bool, // +s (oper: receive server notices)
pub callerid: bool, // +g (only accept PMs from users on the ACCEPT list) 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<String>, // AWAY message, if set pub away: Option<String>, // AWAY message, if set
} }
@ -75,6 +77,12 @@ impl UserFlags {
if self.callerid { if self.callerid {
s.push('g'); s.push('g');
} }
if self.showwhois {
s.push('W');
}
if self.deny_uncommon {
s.push('c');
}
s s
} }
} }
@ -409,7 +417,7 @@ impl Server {
uid, uid,
RPL_MYINFO, RPL_MYINFO,
&format!( &format!(
"{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJ", "{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJ",
self.name self.name
), ),
); );