Skip to content

Commit

Permalink
Merge commit 'refs/pull/266/head' of https://github.com/serenity-rs/p…
Browse files Browse the repository at this point in the history
…oise into feat/user_apps
  • Loading branch information
jamesbt365 committed Jun 14, 2024
2 parents 47bbbad + a8a987d commit cd3d50b
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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", "poise_macros/unstable"]

[package.metadata.docs.rs]
all-features = true
Expand Down
13 changes: 13 additions & 0 deletions examples/feature_showcase/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error + Send + Sync>;
Expand Down Expand Up @@ -84,6 +87,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()),
Expand Down
60 changes: 60 additions & 0 deletions examples/feature_showcase/user_apps.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
3 changes: 3 additions & 0 deletions macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ syn = { version = "2", features = ["fold"] }
quote = "1.0.9"
proc-macro2 = "1.0.24"
darling = "0.20"

[features]
unstable = []
108 changes: 108 additions & 0 deletions macros/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ pub struct CommandArgs {
category: Option<String>,
custom_data: Option<syn::Expr>,

#[cfg(feature = "unstable")]
install_context: Option<syn::punctuated::Punctuated<syn::Ident, syn::Token![|]>>,
#[cfg(feature = "unstable")]
interaction_context: Option<syn::punctuated::Punctuated<syn::Ident, syn::Token![|]>>,

// In seconds
global_cooldown: Option<u64>,
user_cooldown: Option<u64>,
Expand Down Expand Up @@ -97,6 +102,10 @@ 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,
}

Expand Down Expand Up @@ -216,6 +225,38 @@ 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::punctuated::Punctuated<syn::Ident, syn::Token![|]>>,
) -> 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 },
}
}

#[cfg(feature = "unstable")]
let install_context = build_install_context(&args.install_context);

#[cfg(feature = "unstable")]
fn build_interaction_context(
contexts: &Option<syn::punctuated::Punctuated<syn::Ident, syn::Token![|]>>,
) -> 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 },
}
}

#[cfg(feature = "unstable")]
let interaction_context = build_interaction_context(&args.interaction_context);

let inv = Invocation {
parameters,
description,
Expand All @@ -225,6 +266,10 @@ pub fn command(
default_member_permissions,
required_permissions,
required_bot_permissions,
#[cfg(feature = "unstable")]
install_context,
#[cfg(feature = "unstable")]
interaction_context,
};

Ok(TokenStream::from(generate_command(inv)?))
Expand Down Expand Up @@ -291,6 +336,11 @@ fn generate_command(mut inv: Invocation) -> Result<proc_macro2::TokenStream, dar
let dm_only = inv.args.dm_only;
let nsfw_only = inv.args.nsfw_only;

#[cfg(feature = "unstable")]
let install_context = &inv.install_context;
#[cfg(feature = "unstable")]
let interaction_context = &inv.interaction_context;

let help_text = match &inv.args.help_text_fn {
Some(help_text_fn) => quote::quote! { Some(#help_text_fn()) },
None => match &inv.help_text {
Expand Down Expand Up @@ -329,6 +379,64 @@ fn generate_command(mut inv: Invocation) -> Result<proc_macro2::TokenStream, dar
let function_generics = &inv.function.sig.generics;
let function_visibility = &inv.function.vis;
let function = &inv.function;

#[cfg(feature = "unstable")]
return Ok(quote::quote! {
#[allow(clippy::str_to_string)]
#function_visibility fn #function_ident #function_generics() -> ::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,
install_context: #install_context,
interaction_context: #interaction_context,
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: (),
}
}
});

#[cfg(not(feature = "unstable"))]
Ok(quote::quote! {
#[allow(clippy::str_to_string)]
#function_visibility fn #function_ident #function_generics() -> ::poise::Command<
Expand Down
2 changes: 2 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions src/structs/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ pub struct Command<U, E> {
pub context_menu_name: Option<String>,
/// 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<Vec<serenity::InstallationContext>>,
#[cfg(feature = "unstable")]
/// List of interaction contexts for this command (application-only)
pub interaction_context: Option<Vec<serenity::InteractionContext>>,

// Like #[non_exhaustive], but #[poise::command] still needs to be able to create an instance
#[doc(hidden)]
Expand Down Expand Up @@ -200,6 +206,24 @@ impl<U, E> Command<U, E> {
builder = builder.default_member_permissions(self.default_member_permissions);
}

#[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(interaction_context) = self.interaction_context.clone() {
builder = builder.contexts(interaction_context);
}
}

#[cfg(not(feature = "unstable"))]
if self.guild_only {
builder = builder.dm_permission(false);
}
Expand Down Expand Up @@ -234,6 +258,24 @@ impl<U, E> Command<U, E> {
crate::ContextMenuCommandAction::__NonExhaustive => unreachable!(),
});

#[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(interaction_context) = self.interaction_context.clone() {
builder = builder.contexts(interaction_context);
}
}

#[cfg(not(feature = "unstable"))]
if self.guild_only {
builder = builder.dm_permission(false);
}
Expand Down

0 comments on commit cd3d50b

Please sign in to comment.