added more graceful handling of oversized inputs

This commit is contained in:
Lilith Schier 2026-08-20 18:01:31 +02:00
parent e0cf21d3db
commit 0eec30fe82
3 changed files with 238 additions and 180 deletions

View file

@ -1,13 +1,18 @@
mod dice;
mod limits;
mod parsing;
use anyhow::bail;
use crate::dice::{DiscordMd, Witness};
use anyhow::{Context as AnyCtx, bail};
use dotenv::dotenv;
use limits::*;
use rand::{Rng, SeedableRng};
use serenity::all::{Command, CommandInteraction, CommandOptionType, CreateCommandOption, CreateComponent, FullEvent, Interaction, InteractionContext, MessageFlags, ResolvedOption, ResolvedValue};
use serenity::all::{
Command, CommandInteraction, CommandOptionType, CreateCommandOption, CreateComponent,
FullEvent, Interaction, InteractionContext, MessageFlags, ResolvedOption, ResolvedValue,
};
use serenity::builder::{
CreateCommand, CreateContainer, CreateContainerComponent, CreateInteractionResponse,
CreateInteractionResponseFollowup, CreateInteractionResponseMessage, CreateTextDisplay,
CreateCommand, CreateContainer, CreateContainerComponent, CreateInteractionResponse, CreateInteractionResponseMessage, CreateTextDisplay,
};
use serenity::{async_trait, prelude::*};
use std::sync::Arc;
@ -98,7 +103,7 @@ impl EventHandler for Handler {
.add_context(InteractionContext::BotDm)
.add_context(InteractionContext::PrivateChannel),
)
.await;
.await;
debug!("I created the following global slash command: {global_command:#?}");
@ -110,141 +115,148 @@ impl EventHandler for Handler {
}
pub async fn roll(ctx: &Context, command: &CommandInteraction) -> anyhow::Result<()> {
let options = command.data.options();
let Some(ResolvedOption {
value: ResolvedValue::String(formula),
..
}) = options.iter().find(|option| option.name == "formula")
else {
bail!("Formula was not provided.")
};
let repeat = options
.iter()
.find(|option| option.name == "repeat")
.and_then(|option| {
if let ResolvedValue::Integer(repeat) = option.value {
Some(repeat)
} else {
None
}
})
.unwrap_or(1)
.min(20);
let private = options
.iter()
.find(|option| option.name == "private")
.and_then(|option| {
if let ResolvedValue::Boolean(private) = option.value {
Some(private)
} else {
None
}
})
.unwrap_or(false);
let description = options
.iter()
.find(|option| option.name == "description")
.and_then(|option| {
if let ResolvedValue::String(description) = option.value {
Some(description)
} else {
None
}
});
let fixed_seed = options
.iter()
.find(|option| option.name == "seed")
.and_then(|option| {
if let ResolvedValue::String(fixed_seed) = option.value {
Some(fixed_seed)
} else {
None
}
})
.and_then(|s| s.parse::<u64>().ok());
let result = parsing::parse(&formula);
let expression = match result {
Ok(expression) => expression,
Err(error) => {
command
.create_response(
&ctx.http,
CreateInteractionResponse::Message(
CreateInteractionResponseMessage::default()
.content(format!("Dice formula could not be parsed.\n{}", error))
.ephemeral(true),
),
)
.await?;
return Ok(());
}
};
if private {
command.defer_ephemeral(&ctx.http).await?;
} else {
command.defer(&ctx.http).await?;
}
let seed = fixed_seed.unwrap_or_else(|| rand::rng().next_u64());
let mut rng = rand_xoshiro::Xoshiro256PlusPlus::seed_from_u64(seed);
let average = match expression.average(&mut rng) {
Ok(res) => res,
Err(err) => {
command
.create_followup(
&ctx.http,
CreateInteractionResponseFollowup::new().content(format!("{}", err)),
)
.await?;
return Err(err);
}
};
let ev = (average * 10.0).round() / 10.0;
let mut text_components = Vec::with_capacity((repeat + 2) as usize);
if let Some(description) = description {
text_components.push(CreateContainerComponent::TextDisplay(
CreateTextDisplay::new(format!(">>> {description}\n")),
));
}
for _ in 0..repeat {
let result = match expression.collect_evaluation(&mut rng) {
Ok(res) => res,
Err(err) => {
command
.create_followup(
&ctx.http,
CreateInteractionResponseFollowup::new().content(format!("{}", err)),
)
.await?;
return Err(err);
}
pub async fn inner(ctx: &Context, command: &CommandInteraction) -> anyhow::Result<()> {
let options = command.data.options();
let Some(ResolvedOption {
value: ResolvedValue::String(formula),
..
}) = options.iter().find(|option| option.name == "formula")
else {
bail!("Formula was not provided.")
};
text_components.push(CreateContainerComponent::TextDisplay(
CreateTextDisplay::new(format!("{}\n", result)),
));
}
if fixed_seed.is_some() {
text_components.push(CreateContainerComponent::TextDisplay(
CreateTextDisplay::new("***⚠️ This is not a fair roll. The result was generated based on a fixed seed and is thus fully reproducible.***".to_string()),
));
}
text_components.push(CreateContainerComponent::TextDisplay(
CreateTextDisplay::new(format!("-# Expected value: {ev} | Seed: {seed}")),
));
let repeat = options
.iter()
.find(|option| option.name == "repeat")
.and_then(|option| {
if let ResolvedValue::Integer(repeat) = option.value {
Some(repeat)
} else {
None
}
})
.unwrap_or(1)
.min(MAX_ROLL_REPEATS as i64);
let private = options
.iter()
.find(|option| option.name == "private")
.and_then(|option| {
if let ResolvedValue::Boolean(private) = option.value {
Some(private)
} else {
None
}
})
.unwrap_or(false);
let description = options
.iter()
.find(|option| option.name == "description")
.and_then(|option| {
if let ResolvedValue::String(description) = option.value {
Some(description)
} else {
None
}
});
let fixed_seed = options
.iter()
.find(|option| option.name == "seed")
.and_then(|option| {
if let ResolvedValue::String(fixed_seed) = option.value {
Some(fixed_seed)
} else {
None
}
})
.and_then(|s| s.parse::<u64>().ok());
command
.create_followup(
&ctx.http,
CreateInteractionResponseFollowup::new()
let expression = parsing::parse(&formula).context("Dice formula could not be parsed.")?;
let seed = fixed_seed.unwrap_or_else(|| rand::rng().next_u64());
let mut rng = rand_xoshiro::Xoshiro256PlusPlus::seed_from_u64(seed);
let average = expression.average(&mut rng)?;
let ev = (average * 10.0).round() / 10.0;
let discord_md = Arc::new(std::sync::RwLock::new(DiscordMd {
buffers: vec![],
dice_written: 0,
}));
if let Some(description) = description {
discord_md
.write()
.unwrap()
.buffers
.push(format!(">>> {description}\n"));
}
for _ in 0..repeat {
let mut witness = DiscordMd::create_witness(&discord_md);
let result = expression.evaluate(&mut rng, &mut witness, i64::MAX)?;
witness.witness_total_result(result)?;
witness.end();
}
if fixed_seed.is_some() {
discord_md
.write()
.unwrap()
.buffers
.push("***⚠️ This is not a fair roll. The result was generated based on a fixed seed and is thus fully reproducible.***".to_string());
}
discord_md
.write()
.unwrap()
.buffers
.push(format!("-# Expected value: {ev} | Seed: {seed}"));
let text_components = Arc::into_inner(discord_md)
.unwrap()
.into_inner()?
.buffers
.into_iter()
.map(|text| CreateContainerComponent::TextDisplay(CreateTextDisplay::new(text)))
.collect::<Vec<_>>();
let response = CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.components(vec![CreateComponent::Container(CreateContainer::new(
text_components,
))])
.ephemeral(private)
.flags(MessageFlags::IS_COMPONENTS_V2 | MessageFlags::SUPPRESS_NOTIFICATIONS),
)
.await?;
.flags(
MessageFlags::IS_COMPONENTS_V2 | MessageFlags::SUPPRESS_NOTIFICATIONS,
),
);
debug!("Returning response: {response:#?}");
command
.create_response(
&ctx.http,
response,
)
.await?;
Ok(())
}
if let Ok(_) = inner(ctx, command).await {
} else if let Err(err) = inner(ctx, command).await {
warn!("Error encountered: {err:?}");
command
.create_response(
&ctx.http,
CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.components(vec![CreateComponent::Container(CreateContainer::new(
vec![CreateContainerComponent::TextDisplay(
CreateTextDisplay::new(err.to_string()),
)],
))])
.ephemeral(true)
.flags(
MessageFlags::IS_COMPONENTS_V2 | MessageFlags::SUPPRESS_NOTIFICATIONS,
),
),
)
.await?
};
Ok(())
}