poise/structs/
command.rs

1//! The Command struct, which stores all information about a single framework command
2
3use crate::{serenity_prelude as serenity, BoxFuture};
4
5/// Type returned from `#[poise::command]` annotated functions, which contains all of the generated
6/// prefix and application commands
7#[derive(derivative::Derivative)]
8#[derivative(Default(bound = ""), Debug(bound = ""))]
9pub struct Command<U, E> {
10    // =============
11    /// Callback to execute when this command is invoked in a prefix context
12    #[derivative(Debug = "ignore")]
13    pub prefix_action: Option<
14        for<'a> fn(
15            crate::PrefixContext<'a, U, E>,
16        ) -> BoxFuture<'a, Result<(), crate::FrameworkError<'a, U, E>>>,
17    >,
18    /// Callback to execute when this command is invoked in a slash context
19    #[derivative(Debug = "ignore")]
20    pub slash_action: Option<
21        for<'a> fn(
22            crate::ApplicationContext<'a, U, E>,
23        ) -> BoxFuture<'a, Result<(), crate::FrameworkError<'a, U, E>>>,
24    >,
25    /// Callback to execute when this command is invoked in a context menu context
26    ///
27    /// The enum variant shows which Discord item this context menu command works on
28    pub context_menu_action: Option<crate::ContextMenuCommandAction<U, E>>,
29
30    // ============= Command type agnostic data
31    /// Subcommands of this command, if any
32    pub subcommands: Vec<Command<U, E>>,
33    /// Require a subcommand to be invoked
34    pub subcommand_required: bool,
35    /// Main name of the command. Aliases (prefix-only) can be set in [`Self::aliases`].
36    pub name: String,
37    /// Localized names with locale string as the key (slash-only)
38    pub name_localizations: std::collections::HashMap<String, String>,
39    /// Full name including parent command names.
40    ///
41    /// Initially set to just [`Self::name`] and properly populated when the framework is started.
42    pub qualified_name: String,
43    /// A string to identify this particular command within a list of commands.
44    ///
45    /// Can be configured via the [`crate::command`] macro (though it's probably not needed for most
46    /// bots). If not explicitly configured, it falls back to the command function name.
47    pub identifying_name: String,
48    /// The name of the `#[poise::command]`-annotated function
49    pub source_code_name: String,
50    /// Identifier for the category that this command will be displayed in for help commands.
51    pub category: Option<String>,
52    /// Whether to hide this command in help menus.
53    pub hide_in_help: bool,
54    /// Short description of the command. Displayed inline in help menus and similar.
55    pub description: Option<String>,
56    /// Localized descriptions with locale string as the key (slash-only)
57    pub description_localizations: std::collections::HashMap<String, String>,
58    /// Multiline description with detailed usage instructions. Displayed in the command specific
59    /// help: `~help command_name`
60    pub help_text: Option<String>,
61    /// if `true`, disables automatic cooldown handling before this commands invocation.
62    ///
63    /// Will override [`crate::FrameworkOptions::manual_cooldowns`] allowing manual cooldowns
64    /// on select commands.
65    pub manual_cooldowns: Option<bool>,
66    /// Handles command cooldowns. Mainly for framework internal use
67    pub cooldowns: std::sync::Mutex<crate::CooldownTracker>,
68    /// Configuration for the [`crate::CooldownTracker`]
69    pub cooldown_config: std::sync::RwLock<crate::CooldownConfig>,
70    /// After the first response, whether to post subsequent responses as edits to the initial
71    /// message
72    ///
73    /// Note: in prefix commands, this only has an effect if
74    /// `crate::PrefixFrameworkOptions::edit_tracker` is set.
75    pub reuse_response: bool,
76    /// Permissions which users must have to invoke this command. Used by Discord to set who can
77    /// invoke this as a slash command. Not used on prefix commands or checked internally.
78    ///
79    /// Set to [`serenity::Permissions::empty()`] by default
80    pub default_member_permissions: serenity::Permissions,
81    /// Permissions which users must have to invoke this command. This is checked internally and
82    /// works for both prefix commands and slash commands.
83    ///
84    /// Set to [`serenity::Permissions::empty()`] by default
85    pub required_permissions: serenity::Permissions,
86    /// Permissions without which command execution will fail. You can set this to fail early and
87    /// give a descriptive error message in case the
88    /// bot hasn't been assigned the minimum permissions by the guild admin.
89    ///
90    /// Set to [`serenity::Permissions::empty()`] by default
91    pub required_bot_permissions: serenity::Permissions,
92    /// If true, only users from the [owners list](crate::FrameworkOptions::owners) may use this
93    /// command.
94    pub owners_only: bool,
95    /// If true, only people in guilds may use this command
96    pub guild_only: bool,
97    /// If true, the command may only run in DMs
98    pub dm_only: bool,
99    /// If true, the command may only run in NSFW channels
100    pub nsfw_only: bool,
101    /// Command-specific override for [`crate::FrameworkOptions::on_error`]
102    #[derivative(Debug = "ignore")]
103    pub on_error: Option<fn(crate::FrameworkError<'_, U, E>) -> BoxFuture<'_, ()>>,
104    /// If any of these functions returns false, this command will not be executed.
105    #[derivative(Debug = "ignore")]
106    pub checks: Vec<fn(crate::Context<'_, U, E>) -> BoxFuture<'_, Result<bool, E>>>,
107    /// List of parameters for this command
108    ///
109    /// Used for registering and parsing slash commands. Can also be used in help commands
110    pub parameters: Vec<crate::CommandParameter<U, E>>,
111    /// Arbitrary data, useful for storing custom metadata about your commands
112    #[derivative(Default(value = "Box::new(())"))]
113    pub custom_data: Box<dyn std::any::Any + Send + Sync>,
114
115    // ============= Prefix-specific data
116    /// Alternative triggers for the command (prefix-only)
117    pub aliases: Vec<String>,
118    /// Whether to rerun the command if an existing invocation message is edited (prefix-only)
119    pub invoke_on_edit: bool,
120    /// Whether to delete the bot response if an existing invocation message is deleted (prefix-only)
121    pub track_deletion: bool,
122    /// Whether to broadcast a typing indicator while executing this commmand (prefix-only)
123    pub broadcast_typing: bool,
124
125    // ============= Application-specific data
126    /// Context menu specific name for this command, displayed in Discord's context menu
127    pub context_menu_name: Option<String>,
128    /// Whether responses to this command should be ephemeral by default (application-only)
129    pub ephemeral: bool,
130    /// List of installation contexts for this command (application-only)
131    pub install_context: Option<Vec<serenity::InstallationContext>>,
132    /// List of interaction contexts for this command (application-only)
133    pub interaction_context: Option<Vec<serenity::InteractionContext>>,
134
135    // Like #[non_exhaustive], but #[poise::command] still needs to be able to create an instance
136    #[doc(hidden)]
137    pub __non_exhaustive: (),
138}
139
140impl<U, E> PartialEq for Command<U, E> {
141    fn eq(&self, other: &Self) -> bool {
142        std::ptr::eq(self, other)
143    }
144}
145impl<U, E> Eq for Command<U, E> {}
146
147impl<U, E> Command<U, E> {
148    /// Serializes this Command into an application command option, which is the form which Discord
149    /// requires subcommands to be in
150    fn create_as_subcommand(&self) -> Option<serenity::CreateCommandOption> {
151        self.slash_action?;
152
153        let kind = if self.subcommands.is_empty() {
154            serenity::CommandOptionType::SubCommand
155        } else {
156            serenity::CommandOptionType::SubCommandGroup
157        };
158
159        let description = self.description.as_deref().unwrap_or("A slash command");
160        let mut builder = serenity::CreateCommandOption::new(kind, self.name.clone(), description);
161
162        for (locale, name) in &self.name_localizations {
163            builder = builder.name_localized(locale, name);
164        }
165        for (locale, description) in &self.description_localizations {
166            builder = builder.description_localized(locale, description);
167        }
168
169        if self.subcommands.is_empty() {
170            for param in &self.parameters {
171                // Using `?` because if this command has slash-incompatible parameters, we cannot
172                // just ignore them but have to abort the creation process entirely
173                builder = builder.add_sub_option(param.create_as_slash_command_option()?);
174            }
175        } else {
176            for subcommand in &self.subcommands {
177                if let Some(subcommand) = subcommand.create_as_subcommand() {
178                    builder = builder.add_sub_option(subcommand);
179                }
180            }
181        }
182
183        Some(builder)
184    }
185
186    /// Generates a slash command builder from this [`Command`] instance. This can be used
187    /// to register this command on Discord's servers
188    pub fn create_as_slash_command(&self) -> Option<serenity::CreateCommand> {
189        self.slash_action?;
190
191        let mut builder = serenity::CreateCommand::new(self.name.clone())
192            .description(self.description.as_deref().unwrap_or("A slash command"));
193
194        for (locale, name) in &self.name_localizations {
195            builder = builder.name_localized(locale, name);
196        }
197        for (locale, description) in &self.description_localizations {
198            builder = builder.description_localized(locale, description);
199        }
200
201        // This is_empty check is needed because Discord special cases empty
202        // default_member_permissions to mean "admin-only" (yes it's stupid)
203        if !self.default_member_permissions.is_empty() {
204            builder = builder.default_member_permissions(self.default_member_permissions);
205        }
206
207        if self.guild_only {
208            builder = builder.contexts(vec![serenity::InteractionContext::Guild]);
209        } else if self.dm_only {
210            builder = builder.contexts(vec![serenity::InteractionContext::BotDm]);
211        }
212
213        if let Some(install_context) = self.install_context.clone() {
214            builder = builder.integration_types(install_context);
215        }
216
217        if let Some(interaction_context) = self.interaction_context.clone() {
218            builder = builder.contexts(interaction_context);
219        }
220
221        if self.subcommands.is_empty() {
222            for param in &self.parameters {
223                // Using `?` because if this command has slash-incompatible parameters, we cannot
224                // just ignore them but have to abort the creation process entirely
225                builder = builder.add_option(param.create_as_slash_command_option()?);
226            }
227        } else {
228            for subcommand in &self.subcommands {
229                if let Some(subcommand) = subcommand.create_as_subcommand() {
230                    builder = builder.add_option(subcommand);
231                }
232            }
233        }
234
235        Some(builder)
236    }
237
238    /// Generates a context menu command builder from this [`Command`] instance. This can be used
239    /// to register this command on Discord's servers
240    pub fn create_as_context_menu_command(&self) -> Option<serenity::CreateCommand> {
241        let context_menu_action = self.context_menu_action?;
242
243        // TODO: localization?
244        let name = self.context_menu_name.as_deref().unwrap_or(&self.name);
245        let mut builder = serenity::CreateCommand::new(name).kind(match context_menu_action {
246            crate::ContextMenuCommandAction::User(_) => serenity::CommandType::User,
247            crate::ContextMenuCommandAction::Message(_) => serenity::CommandType::Message,
248            crate::ContextMenuCommandAction::__NonExhaustive => unreachable!(),
249        });
250
251        if self.guild_only {
252            builder = builder.contexts(vec![serenity::InteractionContext::Guild]);
253        } else if self.dm_only {
254            builder = builder.contexts(vec![serenity::InteractionContext::BotDm]);
255        }
256
257        if let Some(install_context) = self.install_context.clone() {
258            builder = builder.integration_types(install_context);
259        }
260
261        if let Some(interaction_context) = self.interaction_context.clone() {
262            builder = builder.contexts(interaction_context);
263        }
264
265        Some(builder)
266    }
267}