diff --git a/operserv/src/lib.rs b/operserv/src/lib.rs index 284c74c..46c791f 100644 --- a/operserv/src/lib.rs +++ b/operserv/src/lib.rs @@ -53,6 +53,7 @@ impl Service for OperServ { match args.first().copied() { Some(cmd) if cmd.eq_ignore_ascii_case("AKILL") => xline::AKILL.handle(me, from, args, ctx, db), Some(cmd) if cmd.eq_ignore_ascii_case("SQLINE") => xline::SQLINE.handle(me, from, args, ctx, db), + Some(cmd) if cmd.eq_ignore_ascii_case("SNLINE") => xline::SNLINE.handle(me, from, args, ctx, db), Some(cmd) if cmd.eq_ignore_ascii_case("GLOBAL") => global::handle(me, from, args, ctx), Some(cmd) if cmd.eq_ignore_ascii_case("KILL") => kill::handle(me, from, args, ctx, net), Some(cmd) if cmd.eq_ignore_ascii_case("KICK") => kick::handle(me, from, args, ctx, net), @@ -72,5 +73,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), \x02STATS\x02 (enforcement overview), \x02SVSNICK\x02 , \x02SVSJOIN\x02 <#chan> [key], \x02INFO\x02 ADD|DEL (staff notes), \x02NEWS\x02 ADD|DEL|LIST (announcements), \x02OPER\x02 ADD|DEL|LIST (runtime operators)."); + 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), \x02SNLINE\x02 ADD|DEL|LIST (realname 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), \x02SVSNICK\x02 , \x02SVSJOIN\x02 <#chan> [key], \x02INFO\x02 ADD|DEL (staff notes), \x02NEWS\x02 ADD|DEL|LIST (announcements), \x02OPER\x02 ADD|DEL|LIST (runtime operators)."); } diff --git a/operserv/src/stats.rs b/operserv/src/stats.rs index 80d5b0b..2d0df3e 100644 --- a/operserv/src/stats.rs +++ b/operserv/src/stats.rs @@ -7,6 +7,7 @@ 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 rlines = akills.iter().filter(|a| a.kind == "R").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.")); + ctx.notice(me, from.uid, format!("Live network bans: \x02{glines}\x02 AKILL, \x02{qlines}\x02 SQLINE, \x02{rlines}\x02 SNLINE. Services ignores: \x02{ignores}\x02.")); } diff --git a/operserv/src/xline.rs b/operserv/src/xline.rs index 2a000e9..256b9e4 100644 --- a/operserv/src/xline.rs +++ b/operserv/src/xline.rs @@ -119,6 +119,10 @@ pub const AKILL: Xline = Xline { kind: "G", name: "AKILL", target: "user@host", // SQLINE: a nick Q-line. The mask is a nick glob (no '@'); wildcards allowed. pub const SQLINE: Xline = Xline { kind: "Q", name: "SQLINE", target: "nick", normalize: norm_nick }; +// SNLINE: a realname R-line. The mask is a regex the ircd matches against a +// connecting user's realname (use `.` for spaces, e.g. `.*free.money.*`). +pub const SNLINE: Xline = Xline { kind: "R", name: "SNLINE", target: "realname-regex", normalize: norm_realname }; + fn norm_userhost(input: &str) -> Option { let body = input.rsplit('!').next().unwrap_or(input); let (user, host) = body.split_once('@')?; @@ -137,6 +141,12 @@ fn norm_nick(input: &str) -> Option { Some(input.to_ascii_lowercase()) } +fn norm_realname(input: &str) -> Option { + // A realname regex the ircd evaluates: keep it verbatim (case matters), just + // require it non-empty. A single token — use `.`/`\s` for spaces. + (!input.is_empty()).then(|| input.to_string()) +} + // A mask whose every meaningful character is a wildcard would match nearly all. fn too_wide(mask: &str) -> bool { let trivial = |s: &str| s.chars().all(|c| c == '*' || c == '?' || c == '.' || c == '@'); diff --git a/src/engine/mod.rs b/src/engine/mod.rs index d75d6f6..6ca55d7 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1533,6 +1533,7 @@ impl Engine { fn ban_kind_label(kind: &str) -> &'static str { match kind { "Q" => "nick ban", + "R" => "realname ban", _ => "network ban", } } @@ -3987,6 +3988,8 @@ mod tests { 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"); + // SNLINE drives a realname R-line. + assert!(os(&mut e, "000AAAAAS", "SNLINE ADD .*free.money.* spambot").iter().any(|a| matches!(a, NetAction::AddLine { kind, mask, .. } if kind == "R" && mask == ".*free.money.*")), "R-line added"); // GLOBAL fans out to every user via the $* server glob. let out = os(&mut e, "000AAAAAS", "GLOBAL rebooting in 5");