diff --git a/operserv/src/lib.rs b/operserv/src/lib.rs index b66c81f..26f2b91 100644 --- a/operserv/src/lib.rs +++ b/operserv/src/lib.rs @@ -17,6 +17,8 @@ mod kick; mod mode; #[path = "ignore.rs"] mod ignore; +#[path = "stats.rs"] +mod stats; pub struct OperServ { pub uid: String, @@ -48,6 +50,7 @@ impl Service for OperServ { Some(cmd) if cmd.eq_ignore_ascii_case("KICK") => kick::handle(me, from, args, ctx, net), Some(cmd) if cmd.eq_ignore_ascii_case("MODE") => mode::handle(me, from, args, ctx, net), Some(cmd) if cmd.eq_ignore_ascii_case("IGNORE") => ignore::handle(me, from, args, ctx, db), + Some(cmd) if cmd.eq_ignore_ascii_case("STATS") => stats::handle(me, from, db, ctx), Some(cmd) if cmd.eq_ignore_ascii_case("HELP") => help(me, from, ctx), None => help(me, from, ctx), Some(other) => ctx.notice(me, from.uid, format!("I don't know \x02{other}\x02. Try \x02HELP\x02.")), @@ -56,5 +59,5 @@ impl Service for OperServ { } fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx) { - ctx.notice(me, from.uid, "OperServ holds network operator tools (Priv::Admin): \x02AKILL\x02 ADD|DEL|LIST (user@host bans), \x02SQLINE\x02 ADD|DEL|LIST (nick bans), \x02GLOBAL\x02 (announce to everyone), \x02KILL\x02 [reason] (disconnect a user), \x02KICK\x02 <#chan> [reason], \x02MODE\x02 <#chan> [params], \x02IGNORE\x02 ADD|DEL|LIST (silence a user from services)."); + ctx.notice(me, from.uid, "OperServ holds network operator tools (Priv::Admin): \x02AKILL\x02 ADD|DEL|LIST (user@host bans), \x02SQLINE\x02 ADD|DEL|LIST (nick bans), \x02GLOBAL\x02 (announce to everyone), \x02KILL\x02 [reason] (disconnect a user), \x02KICK\x02 <#chan> [reason], \x02MODE\x02 <#chan> [params], \x02IGNORE\x02 ADD|DEL|LIST (silence a user from services), \x02STATS\x02 (enforcement overview)."); } diff --git a/operserv/src/stats.rs b/operserv/src/stats.rs new file mode 100644 index 0000000..80d5b0b --- /dev/null +++ b/operserv/src/stats.rs @@ -0,0 +1,12 @@ +use fedserv_api::{Sender, ServiceCtx, Store}; + +// STATS: an at-a-glance summary of the enforcement state OperServ holds — how +// many network bans of each kind and how many services ignores are live. +// Read-only, so any operator may run it (the module is already oper-gated). +pub fn handle(me: &str, from: &Sender, db: &mut dyn Store, ctx: &mut ServiceCtx) { + let akills = db.akills(); + let glines = akills.iter().filter(|a| a.kind == "G").count(); + let qlines = akills.iter().filter(|a| a.kind == "Q").count(); + let ignores = db.ignores().len(); + ctx.notice(me, from.uid, format!("Live network bans: \x02{glines}\x02 AKILL, \x02{qlines}\x02 SQLINE. Services ignores: \x02{ignores}\x02.")); +} diff --git a/src/engine/mod.rs b/src/engine/mod.rs index d68581b..26d5c89 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -3947,6 +3947,8 @@ mod tests { assert!(out.iter().any(|a| matches!(a, NetAction::AddLine { kind, mask, .. } if kind == "Q" && mask == "*bot*")), "Q-line added: {out:?}"); // A rejected @-shaped mask (that belongs to AKILL, not SQLINE). assert!(os(&mut e, "000AAAAAS", "SQLINE ADD a@b spam").iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("valid"))), "nick mask rejects user@host"); + // STATS reflects the live ban counts. + assert!(os(&mut e, "000AAAAAS", "STATS").iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("1") && text.contains("SQLINE"))), "stats shows the sqline count"); // GLOBAL fans out to every user via the $* server glob. let out = os(&mut e, "000AAAAAS", "GLOBAL rebooting in 5");