diff --git a/src/engine/db/network.rs b/src/engine/db/network.rs index d74d3f2..354b9bc 100644 --- a/src/engine/db/network.rs +++ b/src/engine/db/network.rs @@ -187,14 +187,18 @@ impl Db { /// channel: a `#`/`&` mask tests the channel name, any other tests the user's /// identity. The engine tests the result for a given event's letter. pub fn notify_flags(&self, target: Option<&echo_api::BanTarget>, chan: Option<&str>) -> String { - // A user on the exclude list (e.g. a `*/*` PyLink relay client) is never - // matched, so relayed traffic can't flood the staff feed. - if let Some(t) = target { - if self.notify_exclude.iter().any(|m| { - echo_api::akick_matches(m, t) || (!m.contains(['@', '!']) && glob_match(m, t.nick)) - }) { - return String::new(); + // Exclusions come first, same mask grammar as a watch: a `#channel` mask + // mutes events in that channel, any other mask mutes a user (e.g. `*/*` for + // PyLink relay clients). A match means the event never reaches the feed. + let excluded = self.notify_exclude.iter().any(|m| { + if m.starts_with('#') || m.starts_with('&') { + chan.is_some_and(|c| glob_match(m, c)) + } else { + target.is_some_and(|t| echo_api::akick_matches(m, t) || (!m.contains(['@', '!']) && glob_match(m, t.nick))) } + }); + if excluded { + return String::new(); } let now = now(); let mut flags = String::new(); diff --git a/src/engine/tests.rs b/src/engine/tests.rs index 89b5cf2..f435c74 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -5384,6 +5384,11 @@ fn notify_flags_match_user_nick_channel_and_expiry() { db.set_notify_exclude(vec!["*/*".to_string()]); assert!(db.notify_flags(Some(&bt("baddie7/fun", "h.example")), None).is_empty(), "relay client excluded from every watch"); assert!(db.notify_flags(Some(&bt("baddie7", "h.example")), None).contains('c'), "a normal nick still matches after exclude is set"); + // A #channel in the exclude list mutes that channel while others still fire — + // e.g. watch every channel's joins but skip #staff. + db.set_notify_exclude(vec!["#staff".to_string()]); + assert!(db.notify_flags(Some(&bt("nice", "h.example")), Some("#staff")).is_empty(), "excluded channel is muted"); + assert!(db.notify_flags(Some(&bt("nice", "h.example")), Some("#warez7")).contains('j'), "other channels still watched"); db.set_notify_exclude(vec![]); // DEL by mask removes just that one. assert!(db.notify_del("baddie*!*@*").unwrap());