From ec3205bf75af25886e18a5c77394a91953da2165 Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 13 Jul 2026 17:12:12 +0000 Subject: [PATCH] BotServ kickers: times-to-ban (TTB) + BANEXPIRE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KICK <#channel> TTB makes the bot ban a user after they've been kicked n times (by any kicker) instead of only kicking; SET <#channel> BANEXPIRE controls how long that ban lasts. One clear per-channel knob rather than Anope's per-kicker ttb array. The kick count rides in the existing ephemeral per-user chatter state, so it's dropped when the user parts or quits. On the ban the bot sets +b on an ideal host mask and, if BANEXPIRE is set, queues an unban that is swept off the next event — no timer thread. Ban time is injectable for tests. --- api/src/lib.rs | 2 + botserv/src/kick.rs | 16 +++- botserv/src/set.rs | 37 ++++++-- src/engine/db.rs | 25 +++++- src/engine/mod.rs | 208 ++++++++++++++++++++++++++++++++------------ 5 files changed, 225 insertions(+), 63 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index a83e64c..3cd10f7 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -556,6 +556,8 @@ pub trait Store { fn set_caps_kicker(&mut self, channel: &str, caps_min: u16, caps_percent: u16) -> Result<(), ChanError>; fn set_flood_kicker(&mut self, channel: &str, lines: u16, secs: u16) -> Result<(), ChanError>; fn set_repeat_kicker(&mut self, channel: &str, times: u16) -> Result<(), ChanError>; + fn set_ttb(&mut self, channel: &str, ttb: u16) -> Result<(), ChanError>; + fn set_ban_expire(&mut self, channel: &str, secs: u32) -> Result<(), ChanError>; // BADWORDS list (regex patterns). add validates the pattern compiles. fn badword_add(&mut self, channel: &str, pattern: &str) -> Result; fn badword_del(&mut self, channel: &str, pattern: &str) -> Result; diff --git a/botserv/src/kick.rs b/botserv/src/kick.rs index ef9bd14..aa59365 100644 --- a/botserv/src/kick.rs +++ b/botserv/src/kick.rs @@ -5,12 +5,26 @@ use fedserv_api::{Kicker, Sender, ServiceCtx, Store}; // and the exemption DONTKICKOPS. Founder-or-admin. pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let (Some(&chan), Some(kind)) = (args.get(1), args.get(2)) else { - ctx.notice(me, from.uid, "Syntax: KICK <#channel> [params]"); + ctx.notice(me, from.uid, "Syntax: KICK <#channel> [params], or KICK <#channel> TTB "); return; }; if !super::require_channel_admin(me, from, chan, ctx, db) { return; } + + // TTB takes a number, not ON/OFF: kicks-before-ban (0 = never ban). + if kind.eq_ignore_ascii_case("TTB") { + match args.get(3).and_then(|s| s.parse::().ok()) { + Some(n) => match db.set_ttb(chan, n) { + Ok(()) if n == 0 => ctx.notice(me, from.uid, format!("The bot will only kick (not ban) in \x02{chan}\x02.")), + Ok(()) => ctx.notice(me, from.uid, format!("The bot will ban a user after \x02{n}\x02 kick(s) in \x02{chan}\x02.")), + Err(_) => reg_error(me, from, chan, ctx), + }, + None => ctx.notice(me, from.uid, "Syntax: KICK <#channel> TTB "), + } + return; + } + let on = match args.get(3).map(|s| s.to_ascii_uppercase()).as_deref() { Some("ON") | Some("TRUE") => true, Some("OFF") | Some("FALSE") => false, diff --git a/botserv/src/set.rs b/botserv/src/set.rs index db85205..084f5a2 100644 --- a/botserv/src/set.rs +++ b/botserv/src/set.rs @@ -1,15 +1,42 @@ -use fedserv_api::{ChanSetting, Sender, ServiceCtx, Store}; +use fedserv_api::{parse_duration, ChanSetting, Sender, ServiceCtx, Store}; -// SET <#channel>