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
132
src/dice.rs
132
src/dice.rs
|
|
@ -1,8 +1,10 @@
|
|||
use crate::limits::{MAX_DICE_COUNT_PER_ROLL, MAX_DISPLAYED_DICE_PER_MESSAGE, RESULT_CULL_CHARACTER_THRESHOLD};
|
||||
use anyhow::bail;
|
||||
use rand::{Rng, RngExt};
|
||||
use std::convert::Infallible;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Write, format};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use thiserror::Error;
|
||||
|
||||
pub type ExpBox = Box<Expression>;
|
||||
|
|
@ -40,16 +42,7 @@ pub enum CompareFragment {
|
|||
Le(ExpBox),
|
||||
}
|
||||
|
||||
const DICE_POOL_LIMIT: usize = 10_000;
|
||||
|
||||
impl Expression {
|
||||
pub fn collect_evaluation(&self, mut rng: impl Rng) -> anyhow::Result<String> {
|
||||
let mut witness = DiscordMdWitness::default();
|
||||
let total = self.evaluate(&mut rng, &mut witness, 100)?;
|
||||
let result_text = witness.buffer;
|
||||
Ok(format!("**{total}** = {result_text}"))
|
||||
}
|
||||
|
||||
pub fn average(&self, rng: &mut impl Rng) -> anyhow::Result<f64> {
|
||||
if let Ok(avg) = self.avg() {
|
||||
return Ok(avg);
|
||||
|
|
@ -61,7 +54,7 @@ impl Expression {
|
|||
Ok(average)
|
||||
}
|
||||
|
||||
fn evaluate<W, E>(
|
||||
pub(crate) fn evaluate<W, E>(
|
||||
&self,
|
||||
rng: &mut impl Rng,
|
||||
w: &mut W,
|
||||
|
|
@ -71,6 +64,7 @@ impl Expression {
|
|||
W: Witness<Error = E>,
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
use crate::limits::MAX_DICE_COUNT_PER_ROLL;
|
||||
use Expression::*;
|
||||
let precedence = self.precedence();
|
||||
let needs_parens = precedence > outer_precedence;
|
||||
|
|
@ -96,7 +90,7 @@ impl Expression {
|
|||
} else {
|
||||
1
|
||||
};
|
||||
if count > DICE_POOL_LIMIT {
|
||||
if count > MAX_DICE_COUNT_PER_ROLL {
|
||||
bail!("Too many dice.")
|
||||
};
|
||||
write!(w, "d")?;
|
||||
|
|
@ -128,7 +122,7 @@ impl Expression {
|
|||
}
|
||||
let mut i = 0;
|
||||
while i < rolls.len() {
|
||||
if i > DICE_POOL_LIMIT {
|
||||
if i > MAX_DICE_COUNT_PER_ROLL {
|
||||
bail!("Explosion added too many dice.")
|
||||
}
|
||||
if comparers.len() == 0 {
|
||||
|
|
@ -298,7 +292,7 @@ impl Expression {
|
|||
} else {
|
||||
1
|
||||
};
|
||||
if count > DICE_POOL_LIMIT {
|
||||
if count > MAX_DICE_COUNT_PER_ROLL {
|
||||
bail!("Too many dice.")
|
||||
};
|
||||
let size = size.avg()? as i64;
|
||||
|
|
@ -390,7 +384,7 @@ impl Expression {
|
|||
}
|
||||
}
|
||||
|
||||
trait Witness {
|
||||
pub(crate) trait Witness {
|
||||
type Ok;
|
||||
type Error: Error;
|
||||
type WitnessSet<'a>: WitnessSet<Ok = Self::Ok, Error = Self::Error>
|
||||
|
|
@ -405,9 +399,10 @@ trait Witness {
|
|||
{
|
||||
self.witness_source_text(&format(args))
|
||||
}
|
||||
fn witness_total_result(&mut self, result: f64) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
trait WitnessSet {
|
||||
pub(crate) trait WitnessSet {
|
||||
type Ok;
|
||||
type Error: Error;
|
||||
|
||||
|
|
@ -416,7 +411,7 @@ trait WitnessSet {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DiceRoll {
|
||||
pub(crate) struct DiceRoll {
|
||||
size: usize,
|
||||
roll: usize,
|
||||
is_admitted: bool,
|
||||
|
|
@ -436,27 +431,56 @@ impl Default for DiceRoll {
|
|||
}
|
||||
}
|
||||
|
||||
struct DiscordMdWitness {
|
||||
buffer: String,
|
||||
dice_written: usize,
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DiscordMd {
|
||||
pub(crate) buffers: Vec<String>,
|
||||
pub(crate) dice_written: usize,
|
||||
}
|
||||
struct DiscordMdWitnessSet<'a> {
|
||||
parent: &'a mut DiscordMdWitness,
|
||||
pub(crate) struct DiscordMdWitness {
|
||||
parent: Arc<RwLock<DiscordMd>>,
|
||||
buffer_idx: usize,
|
||||
result: Option<f64>,
|
||||
}
|
||||
pub(crate) struct DiscordMdWitnessSet<'a> {
|
||||
witness: &'a mut DiscordMdWitness,
|
||||
index: usize,
|
||||
dice_elided: bool,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
enum DiscordMdWitnessError {
|
||||
#[error("formatting failed")]
|
||||
pub(crate) enum DiscordMdWitnessError {
|
||||
#[error("Formatting failed.")]
|
||||
Format(#[from] std::fmt::Error),
|
||||
}
|
||||
|
||||
impl Default for DiscordMdWitness {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer: String::new(),
|
||||
dice_written: 0,
|
||||
impl DiscordMd {
|
||||
pub(crate) fn create_witness(md: &Arc<RwLock<Self>>) -> DiscordMdWitness {
|
||||
let mut parent = md.write().unwrap();
|
||||
parent.buffers.push(String::new());
|
||||
DiscordMdWitness {
|
||||
parent: md.clone(),
|
||||
buffer_idx: parent.buffers.len() - 1,
|
||||
result: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn character_count(&self) -> usize {
|
||||
self.buffers.iter().map(|buf| buf.chars().count()).sum()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl DiscordMdWitness {
|
||||
pub(crate) fn end(self) {
|
||||
let mut parent = self.parent.write().unwrap();
|
||||
if parent.character_count() > RESULT_CULL_CHARACTER_THRESHOLD {
|
||||
parent.buffers[self.buffer_idx] = self
|
||||
.result
|
||||
.map(|result| format!("**{result} = …**"))
|
||||
.unwrap_or_else(|| String::new());
|
||||
} else if let Some(result) = self.result {
|
||||
parent.buffers[self.buffer_idx]
|
||||
.insert_str(0, &format!("**{result}** = "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -467,73 +491,87 @@ impl Witness for DiscordMdWitness {
|
|||
type WitnessSet<'a> = DiscordMdWitnessSet<'a>;
|
||||
|
||||
fn witness_source_text(&mut self, text: &str) -> Result<Self::Ok, Self::Error> {
|
||||
self.buffer.push_str(text);
|
||||
self.parent.write().unwrap().buffers[self.buffer_idx].push_str(text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn witness_set(&mut self) -> Result<Self::WitnessSet<'_>, Self::Error> {
|
||||
write!(self.buffer, "‹ ")?;
|
||||
write!(self.parent.write().unwrap().buffers[self.buffer_idx], "‹ ")?;
|
||||
Ok(DiscordMdWitnessSet {
|
||||
parent: self,
|
||||
witness: self,
|
||||
index: 0,
|
||||
dice_elided: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn witness_total_result(&mut self, result: f64) -> Result<Self::Ok, Self::Error> {
|
||||
self.result = Some(result);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<'a> WitnessSet for DiscordMdWitnessSet<'a> {
|
||||
type Ok = ();
|
||||
type Error = DiscordMdWitnessError;
|
||||
|
||||
fn witness_roll(&mut self, dice: DiceRoll) -> Result<Self::Ok, Self::Error> {
|
||||
if self.parent.dice_written >= 50 {
|
||||
let mut parent = self.witness.parent.write().unwrap();
|
||||
if parent.dice_written >= MAX_DISPLAYED_DICE_PER_MESSAGE {
|
||||
if !self.dice_elided {
|
||||
write!(self.parent.buffer, "…")?;
|
||||
write!(
|
||||
parent.buffers[self.witness.buffer_idx],
|
||||
"…"
|
||||
)?;
|
||||
self.dice_elided = true;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let buf = &mut parent.buffers[self.witness.buffer_idx];
|
||||
let is_extreme = dice.size >= 4 && (dice.roll == 1 || dice.roll == dice.size);
|
||||
// A visible separator is required despite the monospace boxing,
|
||||
// due to iOS Discord being weird.
|
||||
if self.index > 0 {
|
||||
write!(self.parent.buffer, ",")?;
|
||||
write!(buf, ",")?;
|
||||
}
|
||||
if !dice.is_admitted {
|
||||
write!(self.parent.buffer, "~~")?;
|
||||
write!(buf, "~~")?;
|
||||
}
|
||||
if dice.is_from_proliferation {
|
||||
write!(self.parent.buffer, "*")?;
|
||||
write!(buf, "*")?;
|
||||
}
|
||||
if is_extreme {
|
||||
write!(self.parent.buffer, "**")?;
|
||||
write!(buf, "**")?;
|
||||
}
|
||||
if dice.did_proliferate {
|
||||
write!(self.parent.buffer, "__")?;
|
||||
write!(buf, "__")?;
|
||||
}
|
||||
|
||||
write!(self.parent.buffer, "`{}`", dice.roll)?;
|
||||
write!(buf, "`{}`", dice.roll)?;
|
||||
|
||||
if dice.did_proliferate {
|
||||
write!(self.parent.buffer, "__")?;
|
||||
write!(buf, "__")?;
|
||||
}
|
||||
if is_extreme {
|
||||
write!(self.parent.buffer, "**")?;
|
||||
write!(buf, "**")?;
|
||||
}
|
||||
if dice.is_from_proliferation {
|
||||
write!(self.parent.buffer, "*")?;
|
||||
write!(buf, "*")?;
|
||||
}
|
||||
if !dice.is_admitted {
|
||||
write!(self.parent.buffer, "~~")?;
|
||||
write!(buf, "~~")?;
|
||||
}
|
||||
self.index += 1;
|
||||
self.parent.dice_written += 1;
|
||||
parent.dice_written += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
write!(self.parent.buffer, " ›")?;
|
||||
write!(
|
||||
self.witness.parent.write().unwrap().buffers[self.witness.buffer_idx],
|
||||
" ›"
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -553,6 +591,10 @@ impl Witness for std::io::Sink {
|
|||
fn witness_set(&mut self) -> Result<Self::WitnessSet<'_>, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn witness_total_result(&mut self, _result: f64) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WitnessSet for &mut std::io::Sink {
|
||||
|
|
|
|||
4
src/limits.rs
Normal file
4
src/limits.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub const MAX_DICE_COUNT_PER_ROLL: usize = 20_000;
|
||||
pub const RESULT_CULL_CHARACTER_THRESHOLD: usize = 3000;
|
||||
pub const MAX_DISPLAYED_DICE_PER_MESSAGE: usize = 100;
|
||||
pub const MAX_ROLL_REPEATS: usize = 35;
|
||||
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