diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 8aeb0f2..cc47e23 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -390,8 +390,28 @@ impl Engine { if !self.db.notify_flags(Some(&target), chan).contains(flag) { return None; } + // The feed lands in the oper-only staff channel, so a watch line carries the + // forensic detail a cloak hides: the real IP (with rdns if it differs), the + // login, and the origin server. The connection port is not available — the + // ircd's s2s UID burst never sends it. `nick!ident@host` keeps the cloak so + // it still correlates with what ordinary users see. + let mut parts: Vec = Vec::new(); + if !target.ip.is_empty() { + let mut s = format!("\x02{}\x02", target.ip); + if !target.realhost.is_empty() && target.realhost != target.ip { + s.push_str(&format!(" ({})", target.realhost)); + } + parts.push(s); + } + if let Some(account) = target.account { + parts.push(format!("acct \x02{account}\x02")); + } + if !target.server.is_empty() { + parts.push(format!("via {}", target.server)); + } + let detail = if parts.is_empty() { String::new() } else { format!(" · {}", parts.join(" · ")) }; let who = format!("\x02{}\x02!{}@{}", target.nick, target.ident, target.host); - self.feed("NOTIFY", format!("{who} {what}")) + self.feed("NOTIFY", format!("{who} {what}{detail}")) } // Inactivity-expiry thresholds (seconds); None leaves that kind never diff --git a/src/engine/tests.rs b/src/engine/tests.rs index f435c74..4089d8c 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -5408,8 +5408,9 @@ fn notify_hook_emits_feed_for_watched_user() { let out = e.handle(NetEvent::UserAttrs { uid: "000AAAAAC".into(), ident: "x".into(), realhost: "h".into(), gecos: "g".into() }); assert!( out.iter().any(|a| matches!(a, NetAction::Privmsg { from, to, text } - if from == "42SAAAAAO" && to == "#services" && text.contains("[NOTIFY]") && text.contains("connected"))), - "expected a NOTIFY connect line, got {out:?}" + if from == "42SAAAAAO" && to == "#services" && text.contains("[NOTIFY]") && text.contains("connected") + && text.contains("1.2.3.4"))), // the real IP is carried, not just the cloak + "expected a NOTIFY connect line with the real IP, got {out:?}" ); // A clean user draws nothing. e.handle(NetEvent::UserConnect { uid: "000AAAAAD".into(), nick: "nice".into(), host: "h".into(), ip: "9.9.9.9".into() });