Bound dice batches, game count, and report/help queues, and harden community votekick

This commit is contained in:
Jean Chevronnet 2026-07-19 13:34:48 +00:00
parent 64c4bbeae4
commit b2ffc01e52
No known key found for this signature in database
7 changed files with 59 additions and 7 deletions

View file

@ -1,5 +1,17 @@
use super::*;
// Bound a moderation queue (reports / help tickets): when it's at the cap, drop the
// oldest resolved entry — or the oldest entry if all are still open — so a flood
// can't grow it without limit. Called before each push.
fn prune_queue<T>(q: &mut Vec<T>, is_open: impl Fn(&T) -> bool) {
const MAX_QUEUE: usize = 2000;
if q.len() < MAX_QUEUE {
return;
}
let pos = q.iter().position(|e| !is_open(e)).unwrap_or(0);
q.remove(pos);
}
impl Db {
/// Add (or refresh) a `kind` network ban. Returns whether it was newly added.
pub fn akill_add(&mut self, kind: XlineKind, mask: &str, setter: &str, reason: &str, expires: Option<u64>) -> Result<bool, RegError> {
@ -456,6 +468,7 @@ impl Db {
return None;
}
self.report_times.insert(key, now);
prune_queue(&mut self.net.reports, |r| r.open);
let id = self.net.report_seq;
let _ = self.log.append(Event::ReportFiled { id, reporter: reporter.to_string(), target: target.to_string(), reason: reason.to_string(), ts: now });
self.net.report_seq = id + 1;
@ -511,6 +524,7 @@ impl Db {
return None;
}
self.report_times.insert(tkey, now);
prune_queue(&mut self.net.help, |h| h.open);
let id = self.net.help_seq;
let _ = self.log.append(Event::HelpRequested { id, requester: requester.to_string(), message: message.to_string(), ts: now });
self.net.help_seq = id + 1;

View file

@ -213,16 +213,25 @@ impl Engine {
};
let target_nick = target_raw.trim();
if target_nick.is_empty() || target_nick.contains(' ') {
return Some(Vec::new());
return None; // not a real vote — let the line reach the flood/badword kicker
}
let threshold = self.db.channel(channel).map(|c| c.kickers.votekick).unwrap_or(0);
let botnick = self.db.channel(channel).and_then(|c| c.assigned_bot.clone());
let (Some(botnick), true) = (botnick, threshold > 0) else { return Some(Vec::new()) };
let Some(botuid) = self.network.uid_by_nick(&botnick).map(str::to_string) else { return Some(Vec::new()) };
let (Some(botnick), true) = (botnick, threshold > 0) else { return None };
let Some(botuid) = self.network.uid_by_nick(&botnick).map(str::to_string) else { return None };
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.") }]);
};
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
// channel op-access — moderation authority isn't a valid vote target.
let target_account = self.network.account_of(&target_uid).map(str::to_string);
let is_bot = self.bot_uids.values().any(|b| b == &target_uid);
let is_oper = target_account.as_deref().is_some_and(|a| self.oper_privs(a).any());
let is_chanop = target_account.as_deref().is_some_and(|a| self.db.channel(channel).is_some_and(|c| c.is_op(a)));
if is_bot || is_oper || is_chanop {
return Some(vec![NetAction::Privmsg { from: botuid.clone(), to: channel.to_string(), text: format!("\x02{target_display}\x02 can't be voted out.") }]);
}
let now = self.now_secs();
let key = (channel.to_ascii_lowercase(), target_display.to_ascii_lowercase());