added more graceful handling of oversized inputs
This commit is contained in:
parent
e0cf21d3db
commit
0eec30fe82
3 changed files with 238 additions and 180 deletions
282
src/main.rs
282
src/main.rs
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue