From f4a2e4004c904b84028816f7fed862fecfa9727d Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 14 May 2024 02:46:21 +0200 Subject: [PATCH 1/3] Add User Apps --- macros/src/command/mod.rs | 40 +++++++++++++++++++++++++++++++++++++++ src/structs/command.rs | 28 +++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/macros/src/command/mod.rs b/macros/src/command/mod.rs index 8c7f8bbd6136..80682fec57f4 100644 --- a/macros/src/command/mod.rs +++ b/macros/src/command/mod.rs @@ -49,6 +49,9 @@ pub struct CommandArgs { category: Option, custom_data: Option, + install_context: Option>, + interaction_context: Option>, + // In seconds global_cooldown: Option, user_cooldown: Option, @@ -97,6 +100,8 @@ pub struct Invocation { default_member_permissions: syn::Expr, required_permissions: syn::Expr, required_bot_permissions: syn::Expr, + install_context: syn::Expr, + interaction_context: syn::Expr, args: CommandArgs, } @@ -216,6 +221,34 @@ pub fn command( let required_permissions = permissions_to_tokens(&args.required_permissions); let required_bot_permissions = permissions_to_tokens(&args.required_bot_permissions); + fn build_install_context( + contexts: &Option>, + ) -> syn::Expr { + match contexts { + Some(contexts) => { + let contexts = contexts.iter(); + syn::parse_quote! { Some(vec![ #(poise::serenity_prelude::InstallationContext::#contexts),* ]) } + } + None => syn::parse_quote! { None }, + } + } + + let install_context = build_install_context(&args.install_context); + + fn build_interaction_context( + contexts: &Option>, + ) -> syn::Expr { + match contexts { + Some(contexts) => { + let contexts = contexts.iter(); + syn::parse_quote! { Some(vec![ #(poise::serenity_prelude::InteractionContext::#contexts),* ]) } + } + None => syn::parse_quote! { None }, + } + } + + let interaction_context = build_interaction_context(&args.interaction_context); + let inv = Invocation { parameters, description, @@ -225,6 +258,8 @@ pub fn command( default_member_permissions, required_permissions, required_bot_permissions, + install_context, + interaction_context, }; Ok(TokenStream::from(generate_command(inv)?)) @@ -291,6 +326,9 @@ fn generate_command(mut inv: Invocation) -> Result quote::quote! { Some(#help_text_fn()) }, None => match &inv.help_text { @@ -364,6 +402,8 @@ fn generate_command(mut inv: Invocation) -> Result { pub context_menu_name: Option, /// Whether responses to this command should be ephemeral by default (application-only) pub ephemeral: bool, + /// List of installation contexts for this command (application-only) + pub install_context: Option>, + /// List of interaction contexts for this command (application-only) + pub interaction_context: Option>, // Like #[non_exhaustive], but #[poise::command] still needs to be able to create an instance #[doc(hidden)] @@ -196,7 +200,17 @@ impl Command { } if self.guild_only { - builder = builder.dm_permission(false); + builder = builder.contexts(vec![serenity::InteractionContext::Guild]); + } else if self.dm_only { + builder = builder.contexts(vec![serenity::InteractionContext::BotDm]); + } + + if let Some(install_context) = self.install_context.clone() { + builder = builder.integration_types(install_context); + } + + if let Some(interaction_context) = self.interaction_context.clone() { + builder = builder.contexts(interaction_context); } if self.subcommands.is_empty() { @@ -230,7 +244,17 @@ impl Command { }); if self.guild_only { - builder = builder.dm_permission(false); + builder = builder.contexts(vec![serenity::InteractionContext::Guild]); + } else if self.dm_only { + builder = builder.contexts(vec![serenity::InteractionContext::BotDm]); + } + + if let Some(install_context) = self.install_context.clone() { + builder = builder.integration_types(install_context); + } + + if let Some(interaction_context) = self.interaction_context.clone() { + builder = builder.contexts(interaction_context); } Some(builder) From 524d962cba34f24d73dfe43d595b4c395f7270ad Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 17 May 2024 11:44:44 +0200 Subject: [PATCH 2/3] Add unstable feature flag --- Cargo.toml | 1 + macros/Cargo.toml | 3 ++ macros/src/command/mod.rs | 70 ++++++++++++++++++++++++++++++++++++++- src/structs/command.rs | 54 ++++++++++++++++++++---------- 4 files changed, 109 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 913c2216fbc1..ddefa04d1868 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ collector = [] # This feature exists because some users want to disable the mere possibility of catching panics at # build time for peace of mind. handle_panics = [] +unstable = ["serenity/unstable_discord_api", "poise_macros/unstable"] [package.metadata.docs.rs] all-features = true diff --git a/macros/Cargo.toml b/macros/Cargo.toml index cc3512de1d56..73e740e35f48 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -15,3 +15,6 @@ syn = { version = "2", features = ["fold"] } quote = "1.0.9" proc-macro2 = "1.0.24" darling = "0.20" + +[features] +unstable = [] diff --git a/macros/src/command/mod.rs b/macros/src/command/mod.rs index 80682fec57f4..1ff54683fbea 100644 --- a/macros/src/command/mod.rs +++ b/macros/src/command/mod.rs @@ -49,7 +49,9 @@ pub struct CommandArgs { category: Option, custom_data: Option, + #[cfg(feature = "unstable")] install_context: Option>, + #[cfg(feature = "unstable")] interaction_context: Option>, // In seconds @@ -100,7 +102,9 @@ pub struct Invocation { default_member_permissions: syn::Expr, required_permissions: syn::Expr, required_bot_permissions: syn::Expr, + #[cfg(feature = "unstable")] install_context: syn::Expr, + #[cfg(feature = "unstable")] interaction_context: syn::Expr, args: CommandArgs, } @@ -221,6 +225,7 @@ pub fn command( let required_permissions = permissions_to_tokens(&args.required_permissions); let required_bot_permissions = permissions_to_tokens(&args.required_bot_permissions); + #[cfg(feature = "unstable")] fn build_install_context( contexts: &Option>, ) -> syn::Expr { @@ -233,8 +238,10 @@ pub fn command( } } + #[cfg(feature = "unstable")] let install_context = build_install_context(&args.install_context); + #[cfg(feature = "unstable")] fn build_interaction_context( contexts: &Option>, ) -> syn::Expr { @@ -247,6 +254,7 @@ pub fn command( } } + #[cfg(feature = "unstable")] let interaction_context = build_interaction_context(&args.interaction_context); let inv = Invocation { @@ -258,7 +266,9 @@ pub fn command( default_member_permissions, required_permissions, required_bot_permissions, + #[cfg(feature = "unstable")] install_context, + #[cfg(feature = "unstable")] interaction_context, }; @@ -326,7 +336,9 @@ fn generate_command(mut inv: Invocation) -> Result Result ::poise::Command< <#ctx_type_with_static as poise::_GetGenerics>::U, @@ -417,6 +431,60 @@ fn generate_command(mut inv: Invocation) -> Result ::poise::Command< + <#ctx_type_with_static as poise::_GetGenerics>::U, + <#ctx_type_with_static as poise::_GetGenerics>::E, + > { + #function + + ::poise::Command { + prefix_action: #prefix_action, + slash_action: #slash_action, + context_menu_action: #context_menu_action, + + subcommands: vec![ #( #subcommands() ),* ], + subcommand_required: #subcommand_required, + name: #command_name.to_string(), + name_localizations: #name_localizations, + qualified_name: String::from(#command_name), // properly filled in later by Framework + identifying_name: String::from(#identifying_name), + source_code_name: String::from(#function_name), + category: #category, + description: #description, + description_localizations: #description_localizations, + help_text: #help_text, + hide_in_help: #hide_in_help, + cooldowns: std::sync::Mutex::new(::poise::Cooldowns::new()), + cooldown_config: #cooldown_config, + reuse_response: #reuse_response, + default_member_permissions: #default_member_permissions, + required_permissions: #required_permissions, + required_bot_permissions: #required_bot_permissions, + owners_only: #owners_only, + guild_only: #guild_only, + dm_only: #dm_only, + nsfw_only: #nsfw_only, + checks: vec![ #( |ctx| Box::pin(#checks(ctx)) ),* ], + on_error: #on_error, + parameters: vec![ #( #parameters ),* ], + custom_data: #custom_data, + + aliases: vec![ #( #aliases.to_string(), )* ], + invoke_on_edit: #invoke_on_edit, + track_deletion: #track_deletion, + broadcast_typing: #broadcast_typing, + + context_menu_name: #context_menu_name, + ephemeral: #ephemeral, + __non_exhaustive: (), } } diff --git a/src/structs/command.rs b/src/structs/command.rs index 647e175e6ee1..0dd7b530b720 100644 --- a/src/structs/command.rs +++ b/src/structs/command.rs @@ -122,8 +122,10 @@ pub struct Command { pub context_menu_name: Option, /// Whether responses to this command should be ephemeral by default (application-only) pub ephemeral: bool, + #[cfg(feature = "unstable")] /// List of installation contexts for this command (application-only) pub install_context: Option>, + #[cfg(feature = "unstable")] /// List of interaction contexts for this command (application-only) pub interaction_context: Option>, @@ -199,18 +201,26 @@ impl Command { builder = builder.default_member_permissions(self.default_member_permissions); } - if self.guild_only { - builder = builder.contexts(vec![serenity::InteractionContext::Guild]); - } else if self.dm_only { - builder = builder.contexts(vec![serenity::InteractionContext::BotDm]); - } + #[cfg(feature = "unstable")] + { + if self.guild_only { + builder = builder.contexts(vec![serenity::InteractionContext::Guild]); + } else if self.dm_only { + builder = builder.contexts(vec![serenity::InteractionContext::BotDm]); + } - if let Some(install_context) = self.install_context.clone() { - builder = builder.integration_types(install_context); + if let Some(install_context) = self.install_context.clone() { + builder = builder.integration_types(install_context); + } + + if let Some(interaction_context) = self.interaction_context.clone() { + builder = builder.contexts(interaction_context); + } } - if let Some(interaction_context) = self.interaction_context.clone() { - builder = builder.contexts(interaction_context); + #[cfg(not(feature = "unstable"))] + if self.guild_only { + builder = builder.dm_permission(false); } if self.subcommands.is_empty() { @@ -243,18 +253,26 @@ impl Command { crate::ContextMenuCommandAction::__NonExhaustive => unreachable!(), }); - if self.guild_only { - builder = builder.contexts(vec![serenity::InteractionContext::Guild]); - } else if self.dm_only { - builder = builder.contexts(vec![serenity::InteractionContext::BotDm]); - } + #[cfg(feature = "unstable")] + { + if self.guild_only { + builder = builder.contexts(vec![serenity::InteractionContext::Guild]); + } else if self.dm_only { + builder = builder.contexts(vec![serenity::InteractionContext::BotDm]); + } - if let Some(install_context) = self.install_context.clone() { - builder = builder.integration_types(install_context); + if let Some(install_context) = self.install_context.clone() { + builder = builder.integration_types(install_context); + } + + if let Some(interaction_context) = self.interaction_context.clone() { + builder = builder.contexts(interaction_context); + } } - if let Some(interaction_context) = self.interaction_context.clone() { - builder = builder.contexts(interaction_context); + #[cfg(not(feature = "unstable"))] + if self.guild_only { + builder = builder.dm_permission(false); } Some(builder) From a8a987df8eee449a69c6ca3544347c2c2f3697e5 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 31 May 2024 04:53:07 +0200 Subject: [PATCH 3/3] Docs & example --- examples/feature_showcase/main.rs | 13 ++++++ examples/feature_showcase/user_apps.rs | 60 ++++++++++++++++++++++++++ macros/src/lib.rs | 2 + 3 files changed, 75 insertions(+) create mode 100644 examples/feature_showcase/user_apps.rs diff --git a/examples/feature_showcase/main.rs b/examples/feature_showcase/main.rs index 20c5b3c110c2..0725ad25b4b2 100644 --- a/examples/feature_showcase/main.rs +++ b/examples/feature_showcase/main.rs @@ -19,6 +19,9 @@ mod subcommand_required; mod subcommands; mod track_edits; +#[cfg(feature = "unstable")] +mod user_apps; + use poise::serenity_prelude as serenity; type Error = Box; @@ -75,6 +78,16 @@ async fn main() { subcommand_required::parent_subcommand_required(), track_edits::test_reuse_response(), track_edits::add(), + #[cfg(feature = "unstable")] + user_apps::everywhere(), + #[cfg(feature = "unstable")] + user_apps::everywhere_context(), + #[cfg(feature = "unstable")] + user_apps::user_install(), + #[cfg(feature = "unstable")] + user_apps::not_in_guilds(), + #[cfg(feature = "unstable")] + user_apps::user_install_guild(), ], prefix_options: poise::PrefixFrameworkOptions { prefix: Some("~".into()), diff --git a/examples/feature_showcase/user_apps.rs b/examples/feature_showcase/user_apps.rs new file mode 100644 index 000000000000..3abf1c5ae846 --- /dev/null +++ b/examples/feature_showcase/user_apps.rs @@ -0,0 +1,60 @@ +use crate::{Context, Error}; +use poise::serenity_prelude as serenity; + +// `install_context` determines how the bot has to be installed for a command to be available. +// `interaction_context` determines where a command can be used. + +/// Available everywhere +#[poise::command( + slash_command, + install_context = "Guild|User", + interaction_context = "Guild|BotDm|PrivateChannel" +)] +pub async fn everywhere(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("This command is available everywhere!").await?; + Ok(()) +} + +// also works with `context_menu_command` +/// Available everywhere +#[poise::command( + context_menu_command = "Everywhere", + install_context = "Guild|User", + interaction_context = "Guild|BotDm|PrivateChannel" +)] +pub async fn everywhere_context(ctx: Context<'_>, msg: serenity::Message) -> Result<(), Error> { + msg.reply(ctx, "This context menu is available everywhere!") + .await?; + Ok(()) +} + +/// Available with a user install only +#[poise::command( + slash_command, + install_context = "User", + interaction_context = "Guild|BotDm|PrivateChannel" +)] +pub async fn user_install(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("This command is available only with a user install!") + .await?; + Ok(()) +} + +/// Not available in guilds +#[poise::command( + slash_command, + install_context = "User", + interaction_context = "BotDm|PrivateChannel" +)] +pub async fn not_in_guilds(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("This command is not available in guilds!").await?; + Ok(()) +} + +/// User install only in guilds +#[poise::command(slash_command, install_context = "User", interaction_context = "Guild")] +pub async fn user_install_guild(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("This command is available in guilds only with a user install!") + .await?; + Ok(()) +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 5316651af233..a2e4696cd932 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -37,6 +37,8 @@ for example for command-specific help (i.e. `~help command_name`). Escape newlin - `category`: Category of this command which affects placement in the help command - `custom_data`: Arbitrary expression that will be boxed and stored in `Command::custom_data` - `identifying_name`: Optionally, a unique identifier for this command for your personal usage +- `install_context`: Installation contexts where this command is available (slash-only) (`unstable` feature) +- `interaction_context`: Interaction contexts where this command is available (slash-only) (`unstable` feature) ## Checks