From 745ddd540e220a1e1736b19f5fbfe640c7ce507a Mon Sep 17 00:00:00 2001 From: reverse Date: Fri, 21 Aug 2026 15:10:27 +0000 Subject: [PATCH] xline: show a timed ban's remaining time in the removal notice --- src/server.rs | 13 ++++++++++--- src/xline.rs | 22 +++++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/server.rs b/src/server.rs index 3dcd4c1..a67ae8f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1629,12 +1629,15 @@ mod tests { u.flags.snomask = true; u.flags.snomask_cats = "x".to_string(); } - // permanent G-line, then removed + // permanent G-line, then removed → "permanent", no remaining clause s.add_xline(crate::xline::XKind::Gline, "*@bad.example", 0, "op", "spam"); assert!(s.remove_xline(crate::xline::XKind::Gline, "*@bad.example", "op")); assert!(!s.remove_xline(crate::xline::XKind::Gline, "*@bad.example", "op")); // gone: no re-announce + // timed K-line removed early → reports the time it had left + s.add_xline(crate::xline::XKind::Kline, "*@foo.example", 604800, "op", "temp"); + assert!(s.remove_xline(crate::xline::XKind::Kline, "*@foo.example", "op")); // a timed Z-line whose expiry is forced into the past, then purged - s.add_xline(crate::xline::XKind::Zline, "192.0.2.5", 3600, "op", "temp"); + s.add_xline(crate::xline::XKind::Zline, "192.0.2.5", 3600, "op", "temp2"); for x in s.xlines.iter_mut() { if x.mask == "192.0.2.5" { x.expires = 1; @@ -1644,7 +1647,11 @@ mod tests { let joined: String = std::iter::from_fn(|| orx.try_recv().ok()).collect::>().join("\n"); assert!(joined.contains("XLINE: op added a permanent G-line on *@bad.example: spam"), "add: {joined}"); - assert!(joined.contains("XLINE: op removed a G-line on *@bad.example"), "remove: {joined}"); + assert!(joined.contains("XLINE: op removed a permanent G-line on *@bad.example"), "remove permanent: {joined}"); + assert!( + joined.contains("XLINE: op removed a timed K-line on *@foo.example (") && joined.contains("remaining)"), + "remove timed shows remaining: {joined}" + ); assert!(joined.contains("XLINE: Z-line on 192.0.2.5 expired"), "expire: {joined}"); } diff --git a/src/xline.rs b/src/xline.rs index e824745..ad9c269 100644 --- a/src/xline.rs +++ b/src/xline.rs @@ -294,9 +294,17 @@ impl Server { self.enforce_xlines(); } - /// Remove an x-line by kind + mask; announces it (snomask +x, like the add) and - /// returns whether one was found. `remover` is who took it off. + /// Remove an x-line by kind + mask; announces it (snomask +x, like the add) — + /// reporting how long a timed ban had left to run, so opers see what they cut + /// short — and returns whether one was found. `remover` is who took it off. pub fn remove_xline(&mut self, kind: XKind, mask: &str, remover: &str) -> bool { + let n = now(); + // capture the target's expiry before dropping it, to report the time left + let expires = self + .xlines + .iter() + .find(|x| x.kind == kind && x.mask.eq_ignore_ascii_case(mask)) + .map(|x| x.expires); let before = self.xlines.len(); // case-insensitive: nick/host/channel masks match case-insensitively when // enforced, so removal must too (e.g. remove `CBAN #foo` for a `#Foo` ban). @@ -304,7 +312,15 @@ impl Server { .retain(|x| !(x.kind == kind && x.mask.eq_ignore_ascii_case(mask))); let removed = self.xlines.len() < before; if removed { - self.snotice_c('x', &format!("XLINE: {remover} removed a {}-line on {mask}", kind.tag())); + let tag = kind.tag(); + let detail = match expires { + Some(e) if e > n => { + format!("timed {tag}-line on {mask} ({} remaining)", human_duration(e - n)) + } + Some(e) if e != 0 => format!("timed {tag}-line on {mask} (already expired)"), + _ => format!("permanent {tag}-line on {mask}"), + }; + self.snotice_c('x', &format!("XLINE: {remover} removed a {detail}")); self.save_xlines(); } removed