use crate::{serenity_prelude as serenity, BoxFuture, CowVec};
#[async_trait::async_trait]
pub trait SlashArgument: Sized {
async fn extract(
ctx: &serenity::Context,
interaction: &serenity::CommandInteraction,
value: &serenity::ResolvedValue<'_>,
) -> Result<Self, SlashArgError>;
fn create(builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption;
fn choices() -> CowVec<crate::CommandParameterChoice> {
CowVec::default()
}
}
async fn extract_via_argumentconvert<T>(
ctx: &serenity::Context,
interaction: &serenity::CommandInteraction,
value: &serenity::ResolvedValue<'_>,
) -> Result<T, SlashArgError>
where
T: serenity::ArgumentConvert + Send + Sync,
T::Err: std::error::Error + Send + Sync + 'static,
{
let string = match value {
serenity::ResolvedValue::String(str) => *str,
_ => {
return Err(SlashArgError::CommandStructureMismatch {
description: "expected string",
});
}
};
T::convert(
ctx,
interaction.guild_id,
Some(interaction.channel_id),
string,
)
.await
.map_err(|e| SlashArgError::Parse {
error: e.into(),
input: string.into(),
})
}
macro_rules! argumentconvert_slash_argument {
( $(
$( #[cfg(feature = $feature:literal)] )?
$type:ty,
) *) => {
$(
$( #[cfg(feature = $feature)] )?
#[async_trait::async_trait]
impl SlashArgument for $type {
async fn extract(
ctx: &serenity::Context,
interaction: &serenity::CommandInteraction,
value: &serenity::ResolvedValue<'_>,
) -> Result<Self, SlashArgError> {
extract_via_argumentconvert(ctx, interaction, value).await
}
fn create(builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption {
builder.kind(serenity::CommandOptionType::String)
}
}
)*
}
}
argumentconvert_slash_argument! {
serenity::Message,
serenity::EmojiId, serenity::Emoji,
#[cfg(feature = "cache")]
serenity::GuildId,
#[cfg(feature = "cache")]
serenity::Guild,
}
macro_rules! impl_for_integer {
($($t:ty)*) => { $(
#[async_trait::async_trait]
impl SlashArgument for $t {
async fn extract(
_: &serenity::Context,
_: &serenity::CommandInteraction,
value: &serenity::ResolvedValue<'_>,
) -> Result<$t, SlashArgError> {
match *value {
serenity::ResolvedValue::Integer(x) => x
.try_into()
.map_err(|_| SlashArgError::CommandStructureMismatch {
description: "received out of bounds integer",
}),
_ => Err(SlashArgError::CommandStructureMismatch {
description: "expected integer",
}),
}
}
fn create(builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption {
builder
.min_number_value(f64::max(<$t>::MIN as f64, -9007199254740991.))
.max_number_value(f64::min(<$t>::MAX as f64, 9007199254740991.))
.kind(serenity::CommandOptionType::Integer)
}
}
)* };
}
impl_for_integer!(i8 i16 i32 i64 isize u8 u16 u32 u64 usize);
macro_rules! impl_slash_argument {
($type:ty, |$ctx:pat, $interaction:pat, $slash_param_type:ident ( $($arg:pat),* )| $extractor:expr) => {
#[async_trait::async_trait]
impl SlashArgument for $type {
async fn extract(
$ctx: &serenity::Context,
$interaction: &serenity::CommandInteraction,
value: &serenity::ResolvedValue<'_>,
) -> Result<$type, SlashArgError> {
match *value {
serenity::ResolvedValue::$slash_param_type( $($arg),* ) => Ok( $extractor ),
_ => Err(SlashArgError::CommandStructureMismatch {
description: concat!("expected ", stringify!($slash_param_type))
}),
}
}
fn create(builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption {
builder.kind(serenity::CommandOptionType::$slash_param_type)
}
}
};
}
impl_slash_argument!(f32, |_, _, Number(x)| x as f32);
impl_slash_argument!(f64, |_, _, Number(x)| x);
impl_slash_argument!(bool, |_, _, Boolean(x)| x);
impl_slash_argument!(String, |_, _, String(x)| x.into());
impl_slash_argument!(serenity::Attachment, |_, _, Attachment(att)| att.clone());
impl_slash_argument!(serenity::Member, |ctx, interaction, User(user, _)| {
interaction
.guild_id
.ok_or(SlashArgError::Invalid("cannot use member parameter in DMs"))?
.member(ctx, user.id)
.await
.map_err(SlashArgError::Http)?
});
impl_slash_argument!(serenity::PartialMember, |_, _, User(_, member)| {
member
.ok_or(SlashArgError::Invalid("cannot use member parameter in DMs"))?
.clone()
});
impl_slash_argument!(serenity::User, |_, _, User(user, _)| user.clone());
impl_slash_argument!(serenity::UserId, |_, _, User(user, _)| user.id);
impl_slash_argument!(serenity::Channel, |ctx, _, Channel(channel)| {
channel
.id
.to_channel(ctx)
.await
.map_err(SlashArgError::Http)?
});
impl_slash_argument!(serenity::ChannelId, |_, _, Channel(channel)| channel.id);
impl_slash_argument!(serenity::PartialChannel, |_, _, Channel(channel)| channel
.clone());
impl_slash_argument!(serenity::GuildChannel, |ctx, _, Channel(channel)| {
let channel_res = channel.id.to_channel(ctx).await;
let channel = channel_res.map_err(SlashArgError::Http)?.guild();
channel.ok_or(SlashArgError::Http(serenity::Error::Model(
serenity::ModelError::InvalidChannelType,
)))?
});
impl_slash_argument!(serenity::Role, |_, _, Role(role)| role.clone());
impl_slash_argument!(serenity::RoleId, |_, _, Role(role)| role.id);
#[derive(Debug)]
pub enum SlashArgError {
#[non_exhaustive]
CommandStructureMismatch {
description: &'static str,
},
#[non_exhaustive]
Parse {
error: Box<dyn std::error::Error + Send + Sync>,
input: String,
},
#[non_exhaustive]
Invalid(
&'static str,
),
Http(serenity::Error),
#[doc(hidden)]
__NonExhaustive,
}
#[doc(hidden)]
impl SlashArgError {
pub fn new_command_structure_mismatch(description: &'static str) -> Self {
Self::CommandStructureMismatch { description }
}
pub fn new_parse(error: Box<dyn std::error::Error + Send + Sync>, input: String) -> Self {
Self::Parse { error, input }
}
pub fn to_framework_error<U, E>(
self,
ctx: crate::ApplicationContext<'_, U, E>,
) -> crate::FrameworkError<'_, U, E> {
match self {
Self::CommandStructureMismatch { description } => {
crate::FrameworkError::CommandStructureMismatch { ctx, description }
}
Self::Parse { error, input } => crate::FrameworkError::ArgumentParse {
ctx: ctx.into(),
error,
input: Some(input),
},
Self::Invalid(description) => crate::FrameworkError::ArgumentParse {
ctx: ctx.into(),
error: description.into(),
input: None,
},
Self::Http(error) => crate::FrameworkError::ArgumentParse {
ctx: ctx.into(),
error: error.into(),
input: None,
},
Self::__NonExhaustive => unreachable!(),
}
}
}
impl std::fmt::Display for SlashArgError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CommandStructureMismatch { description } => {
write!(
f,
"Bot author did not register their commands correctly ({description})",
)
}
Self::Parse { error, input } => {
write!(f, "Failed to parse `{input}` as argument: {error}")
}
Self::Invalid(description) => {
write!(f, "You can't use this parameter here: {description}",)
}
Self::Http(error) => {
write!(
f,
"Error occurred while retrieving data from Discord: {error}",
)
}
Self::__NonExhaustive => unreachable!(),
}
}
}
impl std::error::Error for SlashArgError {
fn cause(&self) -> Option<&dyn std::error::Error> {
match self {
Self::Http(error) => Some(error),
Self::Parse { error, input: _ } => Some(&**error),
Self::Invalid { .. } | Self::CommandStructureMismatch { .. } => None,
Self::__NonExhaustive => unreachable!(),
}
}
}
pub trait ContextMenuParameter<U, E> {
fn to_action(
action: fn(
crate::ApplicationContext<'_, U, E>,
Self,
) -> BoxFuture<'_, Result<(), crate::FrameworkError<'_, U, E>>>,
) -> crate::ContextMenuCommandAction<U, E>;
}
impl<U, E> ContextMenuParameter<U, E> for serenity::User {
fn to_action(
action: fn(
crate::ApplicationContext<'_, U, E>,
Self,
) -> BoxFuture<'_, Result<(), crate::FrameworkError<'_, U, E>>>,
) -> crate::ContextMenuCommandAction<U, E> {
crate::ContextMenuCommandAction::User(action)
}
}
impl<U, E> ContextMenuParameter<U, E> for serenity::Message {
fn to_action(
action: fn(
crate::ApplicationContext<'_, U, E>,
Self,
) -> BoxFuture<'_, Result<(), crate::FrameworkError<'_, U, E>>>,
) -> crate::ContextMenuCommandAction<U, E> {
crate::ContextMenuCommandAction::Message(action)
}
}