xline: show a timed ban's remaining time in the removal notice

This commit is contained in:
Jean Chevronnet 2026-08-21 15:10:27 +00:00
parent 3ad67c6b52
commit 745ddd540e
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
2 changed files with 29 additions and 6 deletions

View file

@ -1629,12 +1629,15 @@ mod tests {
u.flags.snomask = true; u.flags.snomask = true;
u.flags.snomask_cats = "x".to_string(); 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"); 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"));
assert!(!s.remove_xline(crate::xline::XKind::Gline, "*@bad.example", "op")); // gone: no re-announce 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 // 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() { for x in s.xlines.iter_mut() {
if x.mask == "192.0.2.5" { if x.mask == "192.0.2.5" {
x.expires = 1; x.expires = 1;
@ -1644,7 +1647,11 @@ mod tests {
let joined: String = let joined: String =
std::iter::from_fn(|| orx.try_recv().ok()).collect::<Vec<_>>().join("\n"); std::iter::from_fn(|| orx.try_recv().ok()).collect::<Vec<_>>().join("\n");
assert!(joined.contains("XLINE: op added a permanent G-line on *@bad.example: spam"), "add: {joined}"); 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}"); assert!(joined.contains("XLINE: Z-line on 192.0.2.5 expired"), "expire: {joined}");
} }

View file

@ -294,9 +294,17 @@ impl Server {
self.enforce_xlines(); self.enforce_xlines();
} }
/// Remove an x-line by kind + mask; announces it (snomask +x, like the add) and /// Remove an x-line by kind + mask; announces it (snomask +x, like the add) —
/// returns whether one was found. `remover` is who took it off. /// 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 { 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(); let before = self.xlines.len();
// case-insensitive: nick/host/channel masks match case-insensitively when // case-insensitive: nick/host/channel masks match case-insensitively when
// enforced, so removal must too (e.g. remove `CBAN #foo` for a `#Foo` ban). // 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))); .retain(|x| !(x.kind == kind && x.mask.eq_ignore_ascii_case(mask)));
let removed = self.xlines.len() < before; let removed = self.xlines.len() < before;
if removed { 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(); self.save_xlines();
} }
removed removed