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 25 commits into
base: current
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
93caa09
feat(macro): add group macro
TitaniumBrain May 15, 2024
8aaaf6c
Remove unnecessary use statement
TitaniumBrain May 16, 2024
6da3446
Refactor group for readability
TitaniumBrain May 16, 2024
26498bf
Refactor is_command_attr
TitaniumBrain May 16, 2024
36bf529
Refactor is_command_attr for simplicity
TitaniumBrain May 16, 2024
5203830
Replace empty quote! with constructor
TitaniumBrain May 17, 2024
3e7ca23
Change comment style
TitaniumBrain May 17, 2024
8874415
Use full path for darling::Error
TitaniumBrain May 17, 2024
fb18eee
Call .to_token_stream directly instead of using macro
TitaniumBrain May 17, 2024
90a243f
Add example group test
TitaniumBrain May 21, 2024
a1cd35c
fix: change CommandGroup to use associated types
TitaniumBrain Jun 13, 2024
ff03d77
Update group_testing example
TitaniumBrain Jun 13, 2024
ecd5725
Update CommandGroup doc
TitaniumBrain Jun 14, 2024
5e8c898
Change None check style
TitaniumBrain Jun 14, 2024
3687fd8
Change example to register commands globally
TitaniumBrain Jun 14, 2024
bb4665c
Merge branch 'serenity-rs:current' into current
TitaniumBrain Jun 15, 2024
b1a0e64
chore(group macro): remove leftover commented code
TitaniumBrain Dec 3, 2024
e5f2d29
Merge branch 'add-group-macro' into current
TitaniumBrain Dec 3, 2024
5ae124d
Remove unnecessary Option
TitaniumBrain Feb 10, 2025
8950b1e
Change println! to eprintln!
TitaniumBrain Feb 10, 2025
3802ab3
Add multiple commands in group test
TitaniumBrain Feb 11, 2025
09239eb
Merge branch 'add-group-macro' into current
TitaniumBrain Feb 11, 2025
ad1c382
Merge branch 'serenity-rs:current' into current
TitaniumBrain Feb 11, 2025
591e8dd
Add missing command attributes due to sync
TitaniumBrain Feb 11, 2025
237ead2
Merge branch 'add-group-macro' into current
TitaniumBrain Feb 11, 2025
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
121 changes: 121 additions & 0 deletions examples/group_testing/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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(())
}
}

TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved
// 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_in_guild(
ctx,
&framework.options().commands,
serenity::GuildId::new(308744621616529410),
)
.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()
}
220 changes: 220 additions & 0 deletions macros/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,226 @@ 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 some removed, because they wouldn't make sense
#[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)
// subcommands: crate::util::List<syn::Path>,
// aliases: crate::util::List<String>,
// subcommand_required: bool,
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>,
// rename: Option<String>,
#[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,
// identifying_name: Option<String>,
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