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}