Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(macro): add group macro #267

Open
wants to merge 16 commits into
base: current
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions examples/group_testing/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use poise::{serenity_prelude as serenity, Command, CommandGroup};
use std::{env::var, sync::Arc, time::Duration, vec};
// Types used by all command functions
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;

// Custom user data passed to all command functions
pub struct Data {}

// Group struct
struct Test {}

#[poise::group(category = "Foo")]
impl Test {
// Just a test
#[poise::command(slash_command, prefix_command, rename = "test")]
async fn test_command(ctx: Context<'_>) -> Result<(), Error> {
let name = ctx.author();
ctx.say(format!("Hello, {}", name)).await?;
Ok(())
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can your add a test for multiple commands, and a test for no commands in a group?

// Handlers
async fn on_error(error: poise::FrameworkError<'_, Data, Error>) {
// This is our custom error handler
// They are many errors that can occur, so we only handle the ones we want to customize
// and forward the rest to the default handler
match error {
poise::FrameworkError::Setup { error, .. } => panic!("Failed to start bot: {:?}", error),
poise::FrameworkError::Command { error, ctx, .. } => {
println!("Error in command `{}`: {:?}", ctx.command().name, error,);
}
error => {
if let Err(e) = poise::builtins::on_error(error).await {
println!("Error while handling error: {}", e)
}
}
}
}

#[tokio::main]
async fn main() {
// FrameworkOptions contains all of poise's configuration option in one struct
// Every option can be omitted to use its default value
// println!("{:#?}", Test::commands());
let commands: Vec<Command<Data, Error>> = Test::commands();

let options = poise::FrameworkOptions {
commands: commands,
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some("--".into()),
edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan(
Duration::from_secs(3600),
))),
..Default::default()
},
// The global error handler for all error cases that may occur
on_error: |error| Box::pin(on_error(error)),
// This code is run before every command
pre_command: |ctx| {
Box::pin(async move {
println!("Executing command {}...", ctx.command().qualified_name);
})
},
// This code is run after a command if it was successful (returned Ok)
post_command: |ctx| {
Box::pin(async move {
println!("Executed command {}!", ctx.command().qualified_name);
})
},
// Every command invocation must pass this check to continue execution
command_check: Some(|ctx| {
Box::pin(async move {
if ctx.author().id == 123456789 {
return Ok(false);
}
Ok(true)
})
}),
// Enforce command checks even for owners (enforced by default)
// Set to true to bypass checks, which is useful for testing
skip_checks_for_owners: false,
event_handler: |_ctx, event, _framework, _data| {
Box::pin(async move {
println!(
"Got an event in event handler: {:?}",
event.snake_case_name()
);
Ok(())
})
},
..Default::default()
};

let framework = poise::Framework::builder()
.setup(move |ctx, _ready, framework| {
Box::pin(async move {
println!("Logged in as {}", _ready.user.name);
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data {})
})
})
.options(options)
.build();
let token = var("DISCORD_TOKEN")
.expect("Missing `DISCORD_TOKEN` env var, see README for more information.");
let intents =
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT;

let client = serenity::ClientBuilder::new(token, intents)
.framework(framework)
.await;

client.unwrap().start().await.unwrap()
}
221 changes: 221 additions & 0 deletions macros/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,227 @@ pub struct CommandArgs {
member_cooldown: Option<u64>,
}

impl CommandArgs {
// Check if a field has the default value
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
value == &T::default()
}

// create a new CommandArgs from self, with default fields replaced by value from GroupArgs
pub fn from_group_args(&self, group_args: &GroupArgs) -> CommandArgs {
CommandArgs {
prefix_command: if Self::is_default(&self.prefix_command) {
group_args.prefix_command
} else {
self.prefix_command
},
slash_command: if Self::is_default(&self.slash_command) {
group_args.slash_command
} else {
self.slash_command
},
context_menu_command: if Self::is_default(&self.context_menu_command) {
group_args.context_menu_command.clone()
} else {
self.context_menu_command.clone()
},
subcommands: self.subcommands.clone(), // `GroupArgs` doesn't have `subcommands`
aliases: self.aliases.clone(), // `GroupArgs` doesn't have `aliases`
subcommand_required: self.subcommand_required, // `GroupArgs` doesn't have `subcommand_required`
invoke_on_edit: if Self::is_default(&self.invoke_on_edit) {
group_args.invoke_on_edit
} else {
self.invoke_on_edit
},
reuse_response: if Self::is_default(&self.reuse_response) {
group_args.reuse_response
} else {
self.reuse_response
},
track_deletion: if Self::is_default(&self.track_deletion) {
group_args.track_deletion
} else {
self.track_deletion
},
track_edits: if Self::is_default(&self.track_edits) {
group_args.track_edits
} else {
self.track_edits
},
broadcast_typing: if Self::is_default(&self.broadcast_typing) {
group_args.broadcast_typing
} else {
self.broadcast_typing
},
help_text_fn: if Self::is_default(&self.help_text_fn) {
group_args.help_text_fn.clone()
} else {
self.help_text_fn.clone()
},
check: if Self::is_default(&self.check) {
group_args.check.clone()
} else {
self.check.clone()
},
on_error: if Self::is_default(&self.on_error) {
group_args.on_error.clone()
} else {
self.on_error.clone()
},
rename: self.rename.clone(), // `GroupArgs` doesn't have `rename`
name_localized: if Self::is_default(&self.name_localized) {
group_args.name_localized.clone()
} else {
self.name_localized.clone()
},
description_localized: if Self::is_default(&self.description_localized) {
group_args.description_localized.clone()
} else {
self.description_localized.clone()
},
discard_spare_arguments: if Self::is_default(&self.discard_spare_arguments) {
group_args.discard_spare_arguments
} else {
self.discard_spare_arguments
},
hide_in_help: if Self::is_default(&self.hide_in_help) {
group_args.hide_in_help
} else {
self.hide_in_help
},
ephemeral: if Self::is_default(&self.ephemeral) {
group_args.ephemeral
} else {
self.ephemeral
},
default_member_permissions: if Self::is_default(&self.default_member_permissions) {
group_args.default_member_permissions.clone()
} else {
self.default_member_permissions.clone()
},
required_permissions: if Self::is_default(&self.required_permissions) {
group_args.required_permissions.clone()
} else {
self.required_permissions.clone()
},
required_bot_permissions: if Self::is_default(&self.required_bot_permissions) {
group_args.required_bot_permissions.clone()
} else {
self.required_bot_permissions.clone()
},
owners_only: if Self::is_default(&self.owners_only) {
group_args.owners_only
} else {
self.owners_only
},
guild_only: if Self::is_default(&self.guild_only) {
group_args.guild_only
} else {
self.guild_only
},
dm_only: if Self::is_default(&self.dm_only) {
group_args.dm_only
} else {
self.dm_only
},
nsfw_only: if Self::is_default(&self.nsfw_only) {
group_args.nsfw_only
} else {
self.nsfw_only
},
identifying_name: self.identifying_name.clone(), // `GroupArgs` doesn't have `identifying_name`
category: if Self::is_default(&self.category) {
group_args.category.clone()
} else {
self.category.clone()
},
custom_data: if Self::is_default(&self.custom_data) {
group_args.custom_data.clone()
} else {
self.custom_data.clone()
},
global_cooldown: if Self::is_default(&self.global_cooldown) {
group_args.global_cooldown
} else {
self.global_cooldown
},
user_cooldown: if Self::is_default(&self.user_cooldown) {
group_args.user_cooldown
} else {
self.user_cooldown
},
guild_cooldown: if Self::is_default(&self.guild_cooldown) {
group_args.guild_cooldown
} else {
self.guild_cooldown
},
channel_cooldown: if Self::is_default(&self.channel_cooldown) {
group_args.channel_cooldown
} else {
self.channel_cooldown
},
member_cooldown: if Self::is_default(&self.member_cooldown) {
group_args.member_cooldown
} else {
self.member_cooldown
},
}
}
}

/// Representation of the group attribute arguments (`#[group(...)]`)
///
/// Same as `CommandArgs`, but with the following removed:
/// - subcommands
/// - aliases
/// - subcommand_required
/// - rename
/// - identifying_name
///
#[derive(Default, Debug, darling::FromMeta)]
#[darling(default)]
pub struct GroupArgs {
prefix_command: bool,
slash_command: bool,
context_menu_command: Option<String>,

// When changing these, document it in parent file!
TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved
// TODO: decide why darling(multiple) feels wrong here but not in e.g. localizations (because
// if it's actually irrational, the inconsistency should be fixed)
invoke_on_edit: bool,
reuse_response: bool,
track_deletion: bool,
track_edits: bool,
broadcast_typing: bool,
help_text_fn: Option<syn::Path>,
#[darling(multiple)]
check: Vec<syn::Path>,
on_error: Option<syn::Path>,
#[darling(multiple)]
name_localized: Vec<crate::util::Tuple2<String>>,
#[darling(multiple)]
description_localized: Vec<crate::util::Tuple2<String>>,
discard_spare_arguments: bool,
hide_in_help: bool,
ephemeral: bool,
default_member_permissions: Option<syn::punctuated::Punctuated<syn::Ident, syn::Token![|]>>,
required_permissions: Option<syn::punctuated::Punctuated<syn::Ident, syn::Token![|]>>,
required_bot_permissions: Option<syn::punctuated::Punctuated<syn::Ident, syn::Token![|]>>,
owners_only: bool,
guild_only: bool,
dm_only: bool,
nsfw_only: bool,
category: Option<String>,
custom_data: Option<syn::Expr>,

// In seconds
global_cooldown: Option<u64>,
user_cooldown: Option<u64>,
guild_cooldown: Option<u64>,
channel_cooldown: Option<u64>,
member_cooldown: Option<u64>,
}

/// Representation of the function parameter attribute arguments
#[derive(Default, Debug, darling::FromMeta)]
#[darling(default)]
Expand Down
Loading