ircv3: close server-support-table gaps — BOT=B ISUPPORT, account-extban (a: matcher + ACCOUNTEXTBAN=a), draft/read-marker cap (gates MARKREAD sync), and no-implicit-names (suppress the post-JOIN NAMES burst)
This commit is contained in:
parent
c9bd8e7492
commit
bcd958d2d3
6 changed files with 75 additions and 5 deletions
|
|
@ -964,7 +964,11 @@ impl Server {
|
|||
let text = t.text.clone();
|
||||
self.numeric(uid, RPL_TOPIC, &format!("{name} :{text}"));
|
||||
}
|
||||
self.send_names(uid, &key);
|
||||
// no-implicit-names: a client that negotiated the cap doesn't want the
|
||||
// automatic NAMES burst after JOIN (it asks with NAMES when it needs it).
|
||||
if !self.users.get(&uid).map(|u| u.caps.no_implicit_names).unwrap_or(false) {
|
||||
self.send_names(uid, &key);
|
||||
}
|
||||
self.replay_chanhistory(uid, &key); // +H: replay recent messages to the joiner
|
||||
self.propagate_join(uid, name, is_new); // tell linked servers this user joined
|
||||
self.events.push_back(Hook::Join(uid, key));
|
||||
|
|
@ -1229,6 +1233,7 @@ impl Server {
|
|||
list.iter().any(|b| {
|
||||
if b.mask.as_bytes().get(1) == Some(&b':') {
|
||||
match b.mask.as_bytes().first() {
|
||||
Some(b'a') => crate::modules::accountban::matches(self, uid, &b.mask[2..]),
|
||||
Some(b'g') => crate::modules::securitygroups::in_group(self, uid, &b.mask[2..]),
|
||||
Some(b'y') => {
|
||||
crate::modules::reputation::score_ban_match(self, uid, &b.mask[2..])
|
||||
|
|
|
|||
16
src/modules/accountban.rs
Normal file
16
src/modules/accountban.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
//! The `a:` matching extban (IRCv3 account-extban): match a user by the services
|
||||
//! account they are logged into. `+b a:spammer` bans the account `spammer` (glob),
|
||||
//! `+e a:trusted` exempts one. A user with no account never matches. Dispatched
|
||||
//! from the channel ban matcher; advertised via `ACCOUNTEXTBAN=a`.
|
||||
|
||||
use crate::channels::glob_match;
|
||||
use crate::server::Server;
|
||||
use crate::Uid;
|
||||
|
||||
/// Does `uid`'s logged-in account match the glob `mask` (the part after `a:`)?
|
||||
pub fn matches(s: &Server, uid: Uid, mask: &str) -> bool {
|
||||
match s.users.get(&uid).and_then(|u| u.account.as_deref()) {
|
||||
Some(acct) => glob_match(mask, acct),
|
||||
None => false, // not logged in — an account ban can't match
|
||||
}
|
||||
}
|
||||
|
|
@ -140,11 +140,14 @@ impl Command for MarkReadCmd {
|
|||
.or_default()
|
||||
.insert(tkey, ts);
|
||||
let line = format!(":{} MARKREAD {target} timestamp={}", s.name, iso_time(ts));
|
||||
// Sync the new marker to every connection under this identity that
|
||||
// negotiated draft/read-marker (multi-device); others never asked
|
||||
// for read-marker traffic.
|
||||
let recips: Vec<Uid> = s
|
||||
.users
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|&p| marker_id(s, p) == id)
|
||||
.iter()
|
||||
.filter(|(&p, u)| u.caps.read_marker && marker_id(s, p) == id)
|
||||
.map(|(&p, _)| p)
|
||||
.collect();
|
||||
for p in recips {
|
||||
s.send(p, line.clone());
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
//! self-contained unit.
|
||||
|
||||
pub mod account_registration;
|
||||
pub mod accountban;
|
||||
pub mod antimixedutf8;
|
||||
pub mod antirandom;
|
||||
pub mod autodrop;
|
||||
|
|
|
|||
|
|
@ -751,7 +751,7 @@ impl Server {
|
|||
let include_oper = self.conf_bool("operprefix", false) || self.conf_bool("ojoin", false);
|
||||
let prefix = crate::modules::customprefix::isupport(include_oper);
|
||||
let mut tokens: Vec<String> = format!(
|
||||
"CHANTYPES=# PREFIX={prefix} CHANMODES=beIgXw,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnprstuz EXTBAN=,Gbcgjmnrsy WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}",
|
||||
"CHANTYPES=# PREFIX={prefix} CHANMODES=beIgXw,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnprstuz EXTBAN=,aGbcgjmnrsy ACCOUNTEXTBAN=a BOT=B WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}",
|
||||
self.network
|
||||
)
|
||||
.split(' ')
|
||||
|
|
@ -1395,6 +1395,43 @@ mod tests {
|
|||
assert!(!ann.iter().any(|l| l.contains("PART")), "no fallback on case-only: {ann:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isupport_advertises_bot_and_account_extban() {
|
||||
let s = srv();
|
||||
let joined = s.isupport_lines().join(" ");
|
||||
assert!(joined.contains("BOT=B"), "bot-mode letter: {joined}");
|
||||
assert!(joined.contains("ACCOUNTEXTBAN=a"), "account-extban token: {joined}");
|
||||
assert!(joined.contains("EXTBAN=,aG"), "'a' listed in EXTBAN: {joined}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_extban_matches_by_account_glob() {
|
||||
let mut s = srv();
|
||||
add_user(&mut s, 1, "ann");
|
||||
s.users.get_mut(&1).unwrap().account = Some("spammer".into());
|
||||
add_user(&mut s, 2, "bob"); // no account
|
||||
assert!(crate::modules::accountban::matches(&s, 1, "spam*"));
|
||||
assert!(!crate::modules::accountban::matches(&s, 1, "other"));
|
||||
assert!(!crate::modules::accountban::matches(&s, 2, "*"), "no account never matches");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_implicit_names_suppresses_the_join_names_burst() {
|
||||
let mut s = srv();
|
||||
let arx = add_user(&mut s, 1, "ann");
|
||||
s.users.get_mut(&1).unwrap().caps.no_implicit_names = true;
|
||||
s.join(1, "#c", None);
|
||||
let al: Vec<String> = arx.try_iter().collect();
|
||||
assert!(al.iter().any(|l| l.contains("JOIN #c")), "still gets its JOIN");
|
||||
assert!(!al.iter().any(|l| l.contains(" 353 ")), "no NAMREPLY: {al:?}");
|
||||
assert!(!al.iter().any(|l| l.contains(" 366 ")), "no ENDOFNAMES");
|
||||
// a client without the cap still gets the implicit NAMES
|
||||
let brx = add_user(&mut s, 2, "bob");
|
||||
s.join(2, "#c", None);
|
||||
let bl: Vec<String> = brx.try_iter().collect();
|
||||
assert!(bl.iter().any(|l| l.contains(" 353 ")) && bl.iter().any(|l| l.contains(" 366 ")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nick_change_reindexes_and_notifies_channel() {
|
||||
let mut s = srv();
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ pub const SUPPORTED_CAPS: &[&str] = &[
|
|||
"reverse.im/filehost",
|
||||
"draft/relaymsg",
|
||||
"draft/channel-rename",
|
||||
"draft/read-marker",
|
||||
"no-implicit-names",
|
||||
"cap-notify",
|
||||
];
|
||||
|
||||
|
|
@ -168,6 +170,8 @@ pub struct Caps {
|
|||
pub filehost: bool, // reverse.im/filehost — knows the file-host extension
|
||||
pub relaymsg: bool, // draft/relaymsg — may use RELAYMSG (bridge relaying)
|
||||
pub channel_rename: bool, // draft/channel-rename — receives RENAME (else PART+JOIN)
|
||||
pub read_marker: bool, // draft/read-marker — MARKREAD sync across the identity
|
||||
pub no_implicit_names: bool, // no-implicit-names — suppress the auto NAMES after JOIN
|
||||
pub cap_notify: bool,
|
||||
}
|
||||
|
||||
|
|
@ -237,6 +241,8 @@ impl Caps {
|
|||
"reverse.im/filehost" => self.filehost,
|
||||
"draft/relaymsg" => self.relaymsg,
|
||||
"draft/channel-rename" => self.channel_rename,
|
||||
"draft/read-marker" => self.read_marker,
|
||||
"no-implicit-names" => self.no_implicit_names,
|
||||
"cap-notify" => self.cap_notify,
|
||||
_ => false,
|
||||
}
|
||||
|
|
@ -273,6 +279,8 @@ impl Caps {
|
|||
"reverse.im/filehost" => &mut self.filehost,
|
||||
"draft/relaymsg" => &mut self.relaymsg,
|
||||
"draft/channel-rename" => &mut self.channel_rename,
|
||||
"draft/read-marker" => &mut self.read_marker,
|
||||
"no-implicit-names" => &mut self.no_implicit_names,
|
||||
"cap-notify" => &mut self.cap_notify,
|
||||
_ => return false,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue