BotServ: KICK <#channel> WARN — one warning before the first kick
With WARN on, a user's first kicker offence gets a private warning from the bot instead of a kick; the next one is kicked (and then TTB can ban). The warned flag rides the existing per-user chatter state.
This commit is contained in:
parent
c8be5b4e95
commit
47b1fd351e
4 changed files with 44 additions and 1 deletions
|
|
@ -425,6 +425,8 @@ pub enum Kicker {
|
|||
Repeat,
|
||||
// Kick lines matching a configured badword regex (patterns via BADWORDS).
|
||||
Badwords,
|
||||
// Modifier: warn once before the first kick.
|
||||
Warn,
|
||||
// Exemption, not a rule: never kick channel operators.
|
||||
DontKickOps,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
|
|||
"REVERSES" => Kicker::Reverses,
|
||||
"ITALICS" => Kicker::Italics,
|
||||
"BADWORDS" => Kicker::Badwords,
|
||||
"WARN" => Kicker::Warn,
|
||||
"DONTKICKOPS" => Kicker::DontKickOps,
|
||||
other => {
|
||||
ctx.notice(me, from.uid, format!("Unknown kicker \x02{other}\x02. Try CAPS, FLOOD, REPEAT, BOLDS, COLORS, UNDERLINES, REVERSES, ITALICS or DONTKICKOPS."));
|
||||
|
|
@ -116,6 +117,7 @@ fn label(kicker: Kicker) -> &'static str {
|
|||
Kicker::Flood => "flood",
|
||||
Kicker::Repeat => "repeat",
|
||||
Kicker::Badwords => "badwords",
|
||||
Kicker::Warn => "warn",
|
||||
Kicker::DontKickOps => "dontkickops",
|
||||
}
|
||||
}
|
||||
|
|
@ -124,6 +126,8 @@ fn toggle(me: &str, from: &Sender, chan: &str, kicker: Kicker, on: bool, ctx: &m
|
|||
match db.set_kicker(chan, kicker, on) {
|
||||
Ok(()) if kicker == Kicker::DontKickOps && on => ctx.notice(me, from.uid, format!("The bot will no longer kick channel operators in \x02{chan}\x02.")),
|
||||
Ok(()) if kicker == Kicker::DontKickOps => ctx.notice(me, from.uid, format!("The bot may again kick channel operators in \x02{chan}\x02.")),
|
||||
Ok(()) if kicker == Kicker::Warn && on => ctx.notice(me, from.uid, format!("The bot will warn once before the first kick in \x02{chan}\x02.")),
|
||||
Ok(()) if kicker == Kicker::Warn => ctx.notice(me, from.uid, format!("The bot will kick without warning in \x02{chan}\x02.")),
|
||||
Ok(()) if on => ctx.notice(me, from.uid, format!("The \x02{}\x02 kicker is now on in \x02{chan}\x02.", label(kicker))),
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("The \x02{}\x02 kicker is now off in \x02{chan}\x02.", label(kicker))),
|
||||
Err(_) => reg_error(me, from, chan, ctx),
|
||||
|
|
|
|||
|
|
@ -355,6 +355,9 @@ pub struct KickerSettings {
|
|||
// How long such a ban lasts, in seconds. 0 = until manually removed.
|
||||
#[serde(default)]
|
||||
pub ban_expire: u32,
|
||||
// Warn a user (once) before kicking them the first time.
|
||||
#[serde(default)]
|
||||
pub warn: bool,
|
||||
// Don't kick channel operators, whatever they send.
|
||||
#[serde(default)]
|
||||
pub dontkickops: bool,
|
||||
|
|
@ -1442,6 +1445,7 @@ impl Db {
|
|||
Kicker::Flood => k.flood = on,
|
||||
Kicker::Repeat => k.repeat = on,
|
||||
Kicker::Badwords => k.badwords = on,
|
||||
Kicker::Warn => k.warn = on,
|
||||
Kicker::DontKickOps => k.dontkickops = on,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ struct ChatterState {
|
|||
last_hash: u64, // case-insensitive hash of the previous line
|
||||
repeats: u16, // consecutive identical lines seen so far (0 on the first)
|
||||
kicks: u16, // times kicked so far (reset when it triggers a ban)
|
||||
warned: bool, // whether the one-time warning has been given
|
||||
}
|
||||
|
||||
// A kicker ban awaiting automatic removal (BANEXPIRE).
|
||||
|
|
@ -1024,7 +1025,7 @@ impl Engine {
|
|||
// the counter map below.
|
||||
let (flood_lines, flood_secs) = k.flood_thresholds();
|
||||
let (flood, repeat, repeat_times) = (k.flood, k.repeat, k.repeat_threshold());
|
||||
let (ttb, ban_expire) = (k.ttb, k.ban_expire);
|
||||
let (ttb, ban_expire, warn) = (k.ttb, k.ban_expire, k.warn);
|
||||
|
||||
// FLOOD/REPEAT (only if nothing has tripped yet), updating counters.
|
||||
if reason.is_none() && (flood || repeat) {
|
||||
|
|
@ -1056,6 +1057,20 @@ impl Engine {
|
|||
|
||||
let Some(reason) = reason else { return Vec::new() };
|
||||
|
||||
// WARN: the first offence gets a private warning from the bot instead of a
|
||||
// kick; the next one is kicked for real.
|
||||
if warn {
|
||||
let already = {
|
||||
let state = self.chatter_entry(channel, from);
|
||||
let w = state.warned;
|
||||
state.warned = true;
|
||||
w
|
||||
};
|
||||
if !already {
|
||||
return vec![NetAction::Notice { from: botuid, to: from.to_string(), text: format!("Please mind the channel rules — {reason} Next time you'll be kicked.") }];
|
||||
}
|
||||
}
|
||||
|
||||
// Times-to-ban: after `ttb` kicks the bot bans (an ideal host mask) before
|
||||
// kicking, and schedules the unban if BANEXPIRE is set.
|
||||
let mut out = Vec::new();
|
||||
|
|
@ -2393,6 +2408,24 @@ mod tests {
|
|||
assert!(says(&bs(&mut e, "KICK #d TEST i love frogs"), "would be kicked"), "badwords copied");
|
||||
}
|
||||
|
||||
// WARN gives a one-time warning before the first real kick.
|
||||
#[test]
|
||||
fn botserv_warn_before_kick() {
|
||||
let (mut e, _p) = kicker_fixture("bswarn");
|
||||
let bs = |e: &mut Engine, t: &str| e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAD".into(), text: t.into() });
|
||||
let say = |e: &mut Engine, t: &str| e.handle(NetEvent::Privmsg { from: "000AAAAAS".into(), to: "#c".into(), text: t.into() });
|
||||
bs(&mut e, "KICK #c CAPS ON");
|
||||
bs(&mut e, "KICK #c WARN ON");
|
||||
let kicked = |out: &[NetAction]| out.iter().any(|a| matches!(a, NetAction::Kick { uid, .. } if uid == "000AAAAAS"));
|
||||
|
||||
// First offence: a warning notice from the bot, no kick.
|
||||
let out = say(&mut e, "SHOUTING THE FIRST TIME");
|
||||
assert!(!kicked(&out), "no kick on first offence");
|
||||
assert!(out.iter().any(|a| matches!(a, NetAction::Notice { from, to, text } if from.starts_with("42SB") && to == "000AAAAAS" && text.contains("Next time"))), "warned: {out:?}");
|
||||
// Second offence: kicked for real.
|
||||
assert!(kicked(&say(&mut e, "SHOUTING THE SECOND TIME")), "kicked on second offence");
|
||||
}
|
||||
|
||||
// KICK TEST dry-runs the content kickers against a line without kicking.
|
||||
#[test]
|
||||
fn botserv_kick_test_dry_run() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue