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

@ -9,7 +9,7 @@ use rand::Rng;
// Guardrails so a single expression can't ask us to roll forever.
const MAX_DICE: u64 = 99_999; // dice in one NdM roll
const MAX_SIDES: u64 = 99_999; // sides on a die
const MAX_TOTAL: u64 = 1_000_000; // dice rolled across the whole expression
pub const MAX_TOTAL: u64 = 1_000_000; // dice rolled across the whole expression
// One resolved dice roll, kept for the extended (EX) output.
pub struct Roll {
@ -21,6 +21,7 @@ pub struct Roll {
pub struct Evaluated {
pub value: f64,
pub rolls: Vec<Roll>,
pub total: u64, // dice rolled in this evaluation, for cross-repeat budgeting
}
// ---- tokens ----------------------------------------------------------------
@ -230,7 +231,7 @@ pub fn evaluate(input: &str, rng: &mut impl Rng) -> Result<Evaluated, String> {
if !value.is_finite() {
return Err("that doesn't come out to a real number".to_string());
}
Ok(Evaluated { value, rolls })
Ok(Evaluated { value, rolls, total: total_dice })
}
fn eval(ast: &Ast, rng: &mut impl Rng, rolls: &mut Vec<Roll>, total: &mut u64) -> Result<f64, String> {

View file

@ -27,9 +27,11 @@ pub fn handle(me: &str, from: &Sender, rest: &[&str], ctx: &mut ServiceCtx, exte
};
let mut rng = rand::thread_rng();
let mut spent = 0u64; // dice rolled across the whole repeat batch
for i in 0..times {
match expr::evaluate(body, &mut rng) {
Ok(ev) => {
spent += ev.total;
let result = format_value(ev.value, round_result);
let prefix = if times > 1 { format!("[{}/{}] ", i + 1, times) } else { String::new() };
let detail = if extended && !ev.rolls.is_empty() {
@ -38,6 +40,14 @@ pub fn handle(me: &str, from: &Sender, rest: &[&str], ctx: &mut ServiceCtx, exte
String::new()
};
ctx.notice(me, from.uid, format!("{prefix}\x02{body}\x02 = \x02{result}\x02{detail}"));
// Bound total work across the whole `N~` batch, not per-evaluate, so
// `25~99999d1+…` can't multiply the per-roll cap into tens of millions.
if spent >= expr::MAX_TOTAL {
if i + 1 < times {
ctx.notice(me, from.uid, "That's a lot of dice — stopping the repeats here.");
}
break;
}
}
Err(why) => {
ctx.notice(me, from.uid, format!("I couldn't roll \x02{body}\x02: {why}."));

View file

@ -141,6 +141,14 @@ impl GameServ {
ctx.notice(me, from.uid, "You cannot challenge yourself.");
return;
}
// Global cap: the per-user cap keys on a churnable identity (a guest's nick),
// so a single connection could otherwise cycle nicks to grow the games map
// without bound. Bounding the total makes that impossible regardless.
const MAX_GAMES_TOTAL: usize = 500;
if self.games.len() >= MAX_GAMES_TOTAL {
ctx.notice(me, from.uid, "The game server is at capacity right now. Please try again shortly.");
return;
}
let meid = Self::ident(from).to_string();
let mine = self.games.values().filter(|g| g.acc_a == meid || g.acc_b == meid).count();
if mine >= MAX_GAMES_PER_USER {

View file

@ -7,7 +7,12 @@ pub fn handle(me: &str, from: &Sender, rest: &[&str], ctx: &mut ServiceCtx, db:
ctx.notice(me, from.uid, "Tell us what you need help with: REQUEST <message>");
return;
}
let requester = from.account.unwrap_or(from.nick);
// Require identification, so the cooldown keys on a stable account rather than a
// spoofable nick a flooder can cycle to reset it.
let Some(requester) = from.account else {
ctx.notice(me, from.uid, "Please identify to NickServ before opening a ticket.");
return;
};
match db.help_request(requester, &message) {
Some(id) => ctx.notice(me, from.uid, format!("Thanks — your request (\x02#{id}\x02) is in the queue. A staff member will be with you.")),
None => ctx.notice(me, from.uid, "You just opened a ticket — please wait a moment before opening another."),

View file

@ -11,7 +11,12 @@ pub fn handle(me: &str, from: &Sender, rest: &[&str], ctx: &mut ServiceCtx, db:
return;
}
let reason = reason_words.join(" ");
let reporter = from.account.unwrap_or(from.nick);
// Require identification, so the cooldown keys on a stable account rather than a
// spoofable nick a flooder can cycle to reset it.
let Some(reporter) = from.account else {
ctx.notice(me, from.uid, "Please identify to NickServ before filing a report.");
return;
};
match db.report_file(reporter, target, &reason) {
Some(id) => ctx.notice(me, from.uid, format!("Thanks — your report (\x02#{id}\x02) about \x02{target}\x02 has been sent to the staff.")),
None => ctx.notice(me, from.uid, "You just filed a report — please wait a moment before filing another."),