add SIGNORE: personal mutual server-side ignore across channels and PMs
This commit is contained in:
parent
2c9ce18cd7
commit
6c5f330746
5 changed files with 107 additions and 1 deletions
|
|
@ -599,7 +599,8 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool)
|
||||||
}
|
}
|
||||||
// SILENCE: if the recipient silenced the sender, drop it silently — the
|
// SILENCE: if the recipient silenced the sender, drop it silently — the
|
||||||
// sender is never told (that's the point), but still gets their own echo.
|
// sender is never told (that's the point), but still gets their own echo.
|
||||||
let silenced = s.is_silenced(tuid, &prefix);
|
// SIGNORE is mutual: drop if either party server-ignores the other.
|
||||||
|
let silenced = s.is_silenced(tuid, &prefix) || s.signore_blocks(uid, tuid);
|
||||||
let pm = format!(":{prefix} {cmd} {target} :{text}");
|
let pm = format!(":{prefix} {cmd} {target} :{text}");
|
||||||
let ctags = s.line_ctags.clone();
|
let ctags = s.line_ctags.clone();
|
||||||
let msgid = s.next_msgid();
|
let msgid = s.next_msgid();
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
Box::new(Watch),
|
Box::new(Watch),
|
||||||
Box::new(Monitor),
|
Box::new(Monitor),
|
||||||
Box::new(Silence),
|
Box::new(Silence),
|
||||||
|
Box::new(Signore),
|
||||||
Box::new(Accept),
|
Box::new(Accept),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -298,6 +299,77 @@ impl Command for Silence {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SIGNORE (personal mutual server-side ignore) ---------------------------
|
||||||
|
|
||||||
|
fn signore_list(s: &Server, uid: Uid) {
|
||||||
|
let (nick, list) = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| (u.nick.clone(), u.signore.clone()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let sn = &s.name;
|
||||||
|
if list.is_empty() {
|
||||||
|
s.send(uid, format!(":{sn} NOTICE {nick} :Your SIGNORE list is empty."));
|
||||||
|
} else {
|
||||||
|
for m in &list {
|
||||||
|
s.send(uid, format!(":{sn} NOTICE {nick} :SIGNORE {m}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.send(uid, format!(":{sn} NOTICE {nick} :End of SIGNORE list."));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SIGNORE — a personal, mutual server-side ignore. `SIGNORE <mask>` (or `+mask`)
|
||||||
|
/// blocks a user both ways: neither of you sees the other's channel or private
|
||||||
|
/// messages. `SIGNORE -<mask>` lifts it; a bare `SIGNORE` lists your masks. A bare
|
||||||
|
/// nick becomes `nick!*@*`.
|
||||||
|
struct Signore;
|
||||||
|
impl Command for Signore {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"SIGNORE"
|
||||||
|
}
|
||||||
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||||
|
let Some(arg) = params.first() else {
|
||||||
|
signore_list(s, uid);
|
||||||
|
return CmdResult::Ok;
|
||||||
|
};
|
||||||
|
let nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default();
|
||||||
|
let sn = s.name.clone();
|
||||||
|
let (add, raw) = match arg.strip_prefix('-') {
|
||||||
|
Some(m) => (false, m),
|
||||||
|
None => (true, arg.strip_prefix('+').unwrap_or(arg)),
|
||||||
|
};
|
||||||
|
if raw.is_empty() {
|
||||||
|
signore_list(s, uid);
|
||||||
|
return CmdResult::Ok;
|
||||||
|
}
|
||||||
|
let mask = normalize_mask(raw);
|
||||||
|
if add {
|
||||||
|
let max = s.conf_num("maxsignore", 64usize);
|
||||||
|
let full = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| u.signore.len() >= max && !u.signore.contains(&mask))
|
||||||
|
.unwrap_or(true);
|
||||||
|
if full {
|
||||||
|
s.send(uid, format!(":{sn} NOTICE {nick} :Your SIGNORE list is full ({max} max)."));
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
if !u.signore.contains(&mask) {
|
||||||
|
u.signore.push(mask.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.send(uid, format!(":{sn} NOTICE {nick} :SIGNORE \x02{mask}\x02 added — you and they can no longer see each other's messages."));
|
||||||
|
} else {
|
||||||
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
u.signore.retain(|x| x != &mask);
|
||||||
|
}
|
||||||
|
s.send(uid, format!(":{sn} NOTICE {nick} :SIGNORE \x02{mask}\x02 removed."));
|
||||||
|
}
|
||||||
|
CmdResult::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- ACCEPT (callerid +g allow-list) ----------------------------------------
|
// --- ACCEPT (callerid +g allow-list) ----------------------------------------
|
||||||
|
|
||||||
fn accept_list(s: &Server, uid: Uid) {
|
fn accept_list(s: &Server, uid: Uid) {
|
||||||
|
|
|
||||||
|
|
@ -390,6 +390,7 @@ impl Server {
|
||||||
watch: Vec::new(),
|
watch: Vec::new(),
|
||||||
monitor: Vec::new(),
|
monitor: Vec::new(),
|
||||||
silence: Vec::new(),
|
silence: Vec::new(),
|
||||||
|
signore: Vec::new(),
|
||||||
accept: Vec::new(),
|
accept: Vec::new(),
|
||||||
quitting: None,
|
quitting: None,
|
||||||
flags: UserFlags::default(),
|
flags: UserFlags::default(),
|
||||||
|
|
@ -1200,6 +1201,10 @@ impl Server {
|
||||||
let members: Vec<Uid> = ch.members.keys().copied().collect();
|
let members: Vec<Uid> = ch.members.keys().copied().collect();
|
||||||
let time_tag = format!("time={}", iso_time(now()));
|
let time_tag = format!("time={}", iso_time(now()));
|
||||||
let account = self.users.get(&src).and_then(|su| su.account.clone());
|
let account = self.users.get(&src).and_then(|su| su.account.clone());
|
||||||
|
// SIGNORE (mutual server-ignore): the sender's mask + list, hoisted once.
|
||||||
|
let src_mask = self.users.get(&src).map(|su| su.prefix()).unwrap_or_default();
|
||||||
|
let src_signore: Vec<String> =
|
||||||
|
self.users.get(&src).map(|su| su.signore.clone()).unwrap_or_default();
|
||||||
// one cached line per (server_time, account_tag, message_tags) combination
|
// one cached line per (server_time, account_tag, message_tags) combination
|
||||||
let mut cache: [Option<std::sync::Arc<str>>; 8] = std::array::from_fn(|_| None);
|
let mut cache: [Option<std::sync::Arc<str>>; 8] = std::array::from_fn(|_| None);
|
||||||
for m in members {
|
for m in members {
|
||||||
|
|
@ -1212,6 +1217,13 @@ impl Server {
|
||||||
if u.flags.deaf {
|
if u.flags.deaf {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// SIGNORE: skip a member mutually server-ignored with the sender
|
||||||
|
if (!src_signore.is_empty() || !u.signore.is_empty())
|
||||||
|
&& (src_signore.iter().any(|p| crate::channels::glob_match(p, &u.prefix()))
|
||||||
|
|| u.signore.iter().any(|p| crate::channels::glob_match(p, &src_mask)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if op_only && self.rank(m, key) < crate::channels::RANK_HALFOP {
|
if op_only && self.rank(m, key) < crate::channels::RANK_HALFOP {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1507,6 +1519,7 @@ mod tests {
|
||||||
watch: Vec::new(),
|
watch: Vec::new(),
|
||||||
monitor: Vec::new(),
|
monitor: Vec::new(),
|
||||||
silence: Vec::new(),
|
silence: Vec::new(),
|
||||||
|
signore: Vec::new(),
|
||||||
accept: Vec::new(),
|
accept: Vec::new(),
|
||||||
quitting: None,
|
quitting: None,
|
||||||
flags: UserFlags::default(),
|
flags: UserFlags::default(),
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,7 @@ pub struct User {
|
||||||
pub watch: Vec<String>, // WATCH list — lowercased nicks
|
pub watch: Vec<String>, // WATCH list — lowercased nicks
|
||||||
pub monitor: Vec<String>, // MONITOR list — lowercased nicks
|
pub monitor: Vec<String>, // MONITOR list — lowercased nicks
|
||||||
pub silence: Vec<String>, // SILENCE masks — nick!user@host globs
|
pub silence: Vec<String>, // SILENCE masks — nick!user@host globs
|
||||||
|
pub signore: Vec<String>, // SIGNORE masks — mutual server-side ignore (both ways)
|
||||||
pub accept: Vec<String>, // ACCEPT list — lowercased nicks (callerid +g)
|
pub accept: Vec<String>, // ACCEPT list — lowercased nicks (callerid +g)
|
||||||
pub quitting: Option<String>, // set by QUIT; drained by the core
|
pub quitting: Option<String>, // set by QUIT; drained by the core
|
||||||
pub flags: UserFlags,
|
pub flags: UserFlags,
|
||||||
|
|
|
||||||
19
src/watch.rs
19
src/watch.rs
|
|
@ -208,4 +208,23 @@ impl Server {
|
||||||
.map(|u| u.silence.iter().any(|m| glob_match(m, sender_mask)))
|
.map(|u| u.silence.iter().any(|m| glob_match(m, sender_mask)))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if `a` and `b` mutually SIGNORE each other (either one has the other on
|
||||||
|
/// their SIGNORE list): a bidirectional block — neither sees the other's channel
|
||||||
|
/// or PM messages, triggered by whichever one ran SIGNORE.
|
||||||
|
pub fn signore_blocks(&self, a: Uid, b: Uid) -> bool {
|
||||||
|
self.signore_one_way(a, b) || self.signore_one_way(b, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does `by`'s SIGNORE list match `who`'s current mask?
|
||||||
|
fn signore_one_way(&self, by: Uid, who: Uid) -> bool {
|
||||||
|
let Some(byu) = self.users.get(&by) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if byu.signore.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mask = self.users.get(&who).map(|u| u.prefix()).unwrap_or_default();
|
||||||
|
byu.signore.iter().any(|m| glob_match(m, &mask))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue