diff --git a/src/coremods/core_message.rs b/src/coremods/core_message.rs index 1b3b23f..854f69a 100644 --- a/src/coremods/core_message.rs +++ b/src/coremods/core_message.rs @@ -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 // 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 ctags = s.line_ctags.clone(); let msgid = s.next_msgid(); diff --git a/src/coremods/core_watch.rs b/src/coremods/core_watch.rs index a849570..6bf96be 100644 --- a/src/coremods/core_watch.rs +++ b/src/coremods/core_watch.rs @@ -14,6 +14,7 @@ pub fn commands() -> Vec> { Box::new(Watch), Box::new(Monitor), Box::new(Silence), + Box::new(Signore), 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 ` (or `+mask`) +/// blocks a user both ways: neither of you sees the other's channel or private +/// messages. `SIGNORE -` 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) ---------------------------------------- fn accept_list(s: &Server, uid: Uid) { diff --git a/src/server.rs b/src/server.rs index fb3f39d..fbb4fa8 100644 --- a/src/server.rs +++ b/src/server.rs @@ -390,6 +390,7 @@ impl Server { watch: Vec::new(), monitor: Vec::new(), silence: Vec::new(), + signore: Vec::new(), accept: Vec::new(), quitting: None, flags: UserFlags::default(), @@ -1200,6 +1201,10 @@ impl Server { let members: Vec = ch.members.keys().copied().collect(); let time_tag = format!("time={}", iso_time(now())); 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 = + self.users.get(&src).map(|su| su.signore.clone()).unwrap_or_default(); // one cached line per (server_time, account_tag, message_tags) combination let mut cache: [Option>; 8] = std::array::from_fn(|_| None); for m in members { @@ -1212,6 +1217,13 @@ impl Server { if u.flags.deaf { 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 { continue; } @@ -1507,6 +1519,7 @@ mod tests { watch: Vec::new(), monitor: Vec::new(), silence: Vec::new(), + signore: Vec::new(), accept: Vec::new(), quitting: None, flags: UserFlags::default(), diff --git a/src/users.rs b/src/users.rs index cf957dd..ae95865 100644 --- a/src/users.rs +++ b/src/users.rs @@ -252,6 +252,7 @@ pub struct User { pub watch: Vec, // WATCH list — lowercased nicks pub monitor: Vec, // MONITOR list — lowercased nicks pub silence: Vec, // SILENCE masks — nick!user@host globs + pub signore: Vec, // SIGNORE masks — mutual server-side ignore (both ways) pub accept: Vec, // ACCEPT list — lowercased nicks (callerid +g) pub quitting: Option, // set by QUIT; drained by the core pub flags: UserFlags, diff --git a/src/watch.rs b/src/watch.rs index 935fdd1..7cf6512 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -208,4 +208,23 @@ impl Server { .map(|u| u.silence.iter().any(|m| glob_match(m, sender_mask))) .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)) + } }