Harden votekick: dedup votes, require member targets, fix trigger refs

This commit is contained in:
Jean Chevronnet 2026-07-19 18:52:26 +00:00
parent a7f13098e3
commit e4a2e76903
No known key found for this signature in database
2 changed files with 14 additions and 3 deletions

View file

@ -157,7 +157,8 @@ impl Engine {
} }
e.last_fired = now; e.last_fired = now;
let mut resp = e.response.clone(); let mut resp = e.response.clone();
for i in 1..caps.len() { // High group numbers first, so `$1` doesn't rewrite the prefix of `$10`.
for i in (1..caps.len()).rev() {
resp = resp.replace(&format!("${i}"), caps.get(i).map_or("", |m| m.as_str())); resp = resp.replace(&format!("${i}"), caps.get(i).map_or("", |m| m.as_str()));
} }
response = Some(resp.replace("$nick", &nick)); response = Some(resp.replace("$nick", &nick));
@ -220,8 +221,13 @@ impl Engine {
let (Some(botnick), true) = (botnick, threshold > 0) else { return None }; let (Some(botnick), true) = (botnick, threshold > 0) else { return None };
let botuid = self.network.uid_by_nick(&botnick).map(str::to_string)?; let botuid = self.network.uid_by_nick(&botnick).map(str::to_string)?;
let Some(target_uid) = self.network.uid_by_nick(target_nick).map(str::to_string) else { let Some(target_uid) = self.network.uid_by_nick(target_nick).map(str::to_string) else {
return Some(vec![NetAction::Privmsg { from: botuid, to: channel.to_string(), text: format!("There's no \x02{target_nick}\x02 here to vote on.") }]); return None; // unknown target: treat as ordinary text so the bot can't be spammed into flooding, and the flood kicker still sees it
}; };
// Only a current member can be voted out — otherwise `!voteban <online nick
// from another channel>` would ban someone who never visited.
if !self.network.channel_members(channel).any(|u| u == target_uid.as_str()) {
return None;
}
let target_display = self.network.nick_of(&target_uid).unwrap_or(target_nick).to_string(); let target_display = self.network.nick_of(&target_uid).unwrap_or(target_nick).to_string();
// Never let the community vote out a service bot, an oper, or a user with // Never let the community vote out a service bot, an oper, or a user with
// channel op-access — moderation authority isn't a valid vote target. // channel op-access — moderation authority isn't a valid vote target.
@ -242,7 +248,11 @@ impl Engine {
let vs = self.votes.entry(key.clone()).or_insert(VoteState { ban, voters: std::collections::HashSet::new(), started: now }); let vs = self.votes.entry(key.clone()).or_insert(VoteState { ban, voters: std::collections::HashSet::new(), started: now });
vs.ban = ban; vs.ban = ban;
vs.voters.insert(from.to_string()); if !vs.voters.insert(from.to_string()) {
// Already counted this voter: don't re-announce (which let one user spam
// the bot into flooding), and let the line fall through to the flood kicker.
return None;
}
let count = vs.voters.len() as u16; let count = vs.voters.len() as u16;
if count < threshold { if count < threshold {

View file

@ -2898,6 +2898,7 @@
bs(&mut e, "SET #c VOTEKICK 2"); bs(&mut e, "SET #c VOTEKICK 2");
e.handle(NetEvent::UserConnect { uid: "000AAAAAV".into(), nick: "victim".into(), host: "h".into() , ip: "0.0.0.0".into() }); e.handle(NetEvent::UserConnect { uid: "000AAAAAV".into(), nick: "victim".into(), host: "h".into() , ip: "0.0.0.0".into() });
e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "carol".into(), host: "h".into() , ip: "0.0.0.0".into() }); e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "carol".into(), host: "h".into() , ip: "0.0.0.0".into() });
e.handle(NetEvent::Join { uid: "000AAAAAV".into(), channel: "#c".into(), op: false }); // the target must be in the channel
let kicked = |out: &[NetAction]| out.iter().any(|a| matches!(a, NetAction::Kick { from, uid, .. } if from.starts_with("42SB") && uid == "000AAAAAV")); let kicked = |out: &[NetAction]| out.iter().any(|a| matches!(a, NetAction::Kick { from, uid, .. } if from.starts_with("42SB") && uid == "000AAAAAV"));
// First voter, then the same voter again (deduped) — no kick yet. // First voter, then the same voter again (deduped) — no kick yet.