Add OperServ with AKILL network bans

The first operator module. AKILL manages network bans as a typed,
event-sourced list with lazy expiry, mirroring the patterns the other
modules use:

- AKILL ADD [+expiry] <user@host> <reason> stores the ban and drives an
  ircd G-line (applied to matching users already online and to future
  connections); AKILL DEL <mask|number> lifts it; AKILL LIST [pattern]
  shows the live list, numbered. Admin-only.
- Bans are Global events, so every node holds the list and re-asserts each
  one at burst with its remaining duration. Expired bans are hidden lazily
  and dropped at compaction.
- Masks are normalised to user@host (a nick! prefix is stripped, both
  sides lowercased) and an all-wildcard mask is refused.

Email (send_email) is already reachable from the api via ServiceCtx, and
adds AddLine/DelLine to the action vocabulary for any future X-line kinds.
This commit is contained in:
Jean Chevronnet 2026-07-14 00:12:29 +00:00
parent 253d4c2a2d
commit d2c1d076fb
No known key found for this signature in database
12 changed files with 430 additions and 12 deletions

45
operserv/src/lib.rs Normal file
View file

@ -0,0 +1,45 @@
//! OperServ gives services operators network-wide tools. The first is AKILL:
//! network bans (G-lines) that keep matching users off the whole network,
//! event-sourced so they survive a restart and re-apply at burst, with lazy
//! expiry like the rest of the store. `lib.rs` dispatches; each command is its
//! own file.
use fedserv_api::{NetView, Sender, Service, ServiceCtx, Store};
#[path = "akill.rs"]
mod akill;
pub struct OperServ {
pub uid: String,
}
impl Service for OperServ {
fn nick(&self) -> &str {
"OperServ"
}
fn uid(&self) -> &str {
&self.uid
}
fn gecos(&self) -> &str {
"Operator Service"
}
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, _net: &dyn NetView, db: &mut dyn Store) {
let me = self.uid.as_str();
// Every OperServ command is operator-only: reveal nothing to others.
if !from.privs.any() {
ctx.notice(me, from.uid, "Access denied — OperServ is for services operators.");
return;
}
match args.first().copied() {
Some(cmd) if cmd.eq_ignore_ascii_case("AKILL") => akill::handle(me, from, args, ctx, db),
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 \x02AKILL\x02 or \x02HELP\x02.")),
}
}
}
fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx) {
ctx.notice(me, from.uid, "OperServ holds network operator tools. \x02AKILL ADD\x02 [+expiry] <user@host> <reason>, \x02AKILL DEL\x02 <user@host|number>, \x02AKILL LIST\x02 [pattern] — network bans (Priv::Admin).");
}