serenity/model/application/
command.rs

1use std::collections::HashMap;
2
3use serde::Serialize;
4
5use super::{InstallationContext, InteractionContext};
6#[cfg(feature = "model")]
7use crate::builder::{Builder, CreateCommand};
8#[cfg(feature = "model")]
9use crate::http::{CacheHttp, Http};
10use crate::internal::prelude::*;
11use crate::model::channel::ChannelType;
12use crate::model::id::{
13    ApplicationId,
14    CommandId,
15    CommandPermissionId,
16    CommandVersionId,
17    GuildId,
18    RoleId,
19    UserId,
20};
21use crate::model::Permissions;
22
23/// The base command model that belongs to an application.
24///
25/// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure).
26#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
27#[derive(Clone, Debug, Deserialize, Serialize)]
28#[non_exhaustive]
29pub struct Command {
30    /// The command Id.
31    pub id: CommandId,
32    /// The application command kind.
33    #[serde(rename = "type")]
34    pub kind: CommandType,
35    /// The parent application Id.
36    pub application_id: ApplicationId,
37    /// The command guild Id, if it is a guild command.
38    ///
39    /// **Note**: It may only be present if it is received through the gateway.
40    pub guild_id: Option<GuildId>,
41    /// The command name.
42    pub name: String,
43    /// The localized command name of the selected locale.
44    ///
45    /// If the name is localized, either this field or [`Self::name_localizations`] is set,
46    /// depending on which endpoint this data was retrieved from
47    /// ([source](https://discord.com/developers/docs/interactions/application-commands#retrieving-localized-commands)).
48    pub name_localized: Option<String>,
49    /// All localized command names.
50    ///
51    /// If the name is localized, either this field or [`Self::name_localized`] is set, depending
52    /// on which endpoint this data was retrieved from
53    /// ([source](https://discord.com/developers/docs/interactions/application-commands#retrieving-localized-commands)).
54    pub name_localizations: Option<HashMap<String, String>>,
55    /// The command description.
56    pub description: String,
57    /// The localized command description of the selected locale.
58    ///
59    /// If the description is localized, either this field or [`Self::description_localizations`]
60    /// is set, depending on which endpoint this data was retrieved from
61    /// ([source](https://discord.com/developers/docs/interactions/application-commands#retrieving-localized-commands)).
62    pub description_localized: Option<String>,
63    /// All localized command descriptions.
64    ///
65    /// If the description is localized, either this field or [`Self::description_localized`] is
66    /// set, depending on which endpoint this data was retrieved from
67    /// ([source](https://discord.com/developers/docs/interactions/application-commands#retrieving-localized-commands)).
68    pub description_localizations: Option<HashMap<String, String>>,
69    /// The parameters for the command.
70    #[serde(default)]
71    pub options: Vec<CommandOption>,
72    /// The default permissions required to execute the command.
73    pub default_member_permissions: Option<Permissions>,
74    /// Indicates whether the command is available in DMs with the app, only for globally-scoped
75    /// commands. By default, commands are visible.
76    #[serde(default)]
77    #[cfg_attr(
78        all(not(ignore_serenity_deprecated), feature = "unstable_discord_api"),
79        deprecated = "Use Command::contexts"
80    )]
81    pub dm_permission: Option<bool>,
82    /// Indicates whether the command is [age-restricted](https://discord.com/developers/docs/interactions/application-commands#agerestricted-commands),
83    /// defaults to false.
84    #[serde(default)]
85    pub nsfw: bool,
86    /// Installation context(s) where the command is available, only for globally-scoped commands.
87    ///
88    /// Defaults to [`InstallationContext::Guild`] and [`InstallationContext::User`].
89    #[serde(default)]
90    pub integration_types: Vec<InstallationContext>,
91    /// Interaction context(s) where the command can be used, only for globally-scoped commands.
92    ///
93    /// By default, all interaction context types are included.
94    pub contexts: Option<Vec<InteractionContext>>,
95    /// An autoincremented version identifier updated during substantial record changes.
96    pub version: CommandVersionId,
97    /// Only present for commands of type [`PrimaryEntryPoint`].
98    ///
99    /// [`PrimaryEntryPoint`]: CommandType::PrimaryEntryPoint
100    pub handler: Option<EntryPointHandlerType>,
101}
102
103#[cfg(feature = "model")]
104impl Command {
105    /// Create a global [`Command`], overriding an existing one with the same name if it exists.
106    ///
107    /// When a created [`Command`] is used, the [`InteractionCreate`] event will be emitted.
108    ///
109    /// **Note**: Global commands may take up to an hour to be updated in the user slash commands
110    /// list. If an outdated command data is sent by a user, discord will consider it as an error
111    /// and then will instantly update that command.
112    ///
113    /// As such, it is recommended that guild application commands be used for testing purposes.
114    ///
115    /// # Examples
116    ///
117    /// Create a simple ping command:
118    ///
119    /// ```rust,no_run
120    /// # use serenity::http::Http;
121    /// # use std::sync::Arc;
122    /// #
123    /// # async fn run() {
124    /// # let http: Arc<Http> = unimplemented!();
125    /// use serenity::builder::CreateCommand;
126    /// use serenity::model::application::Command;
127    /// use serenity::model::id::ApplicationId;
128    ///
129    /// let builder = CreateCommand::new("ping").description("A simple ping command");
130    /// let _ = Command::create_global_command(&http, builder).await;
131    /// # }
132    /// ```
133    ///
134    /// Create a command that echoes what is inserted:
135    ///
136    /// ```rust,no_run
137    /// # use serenity::http::Http;
138    /// # use std::sync::Arc;
139    /// #
140    /// # async fn run() {
141    /// # let http: Arc<Http> = unimplemented!();
142    /// use serenity::builder::{CreateCommand, CreateCommandOption as CreateOption};
143    /// use serenity::model::application::{Command, CommandOptionType};
144    /// use serenity::model::id::ApplicationId;
145    ///
146    /// let builder =
147    ///     CreateCommand::new("echo").description("Makes the bot send a message").add_option(
148    ///         CreateOption::new(CommandOptionType::String, "message", "The message to send")
149    ///             .required(true),
150    ///     );
151    /// let _ = Command::create_global_command(&http, builder).await;
152    /// # }
153    /// ```
154    ///
155    /// # Errors
156    ///
157    /// See [`CreateCommand::execute`] for a list of possible errors.
158    ///
159    /// [`InteractionCreate`]: crate::client::EventHandler::interaction_create
160    pub async fn create_global_command(
161        cache_http: impl CacheHttp,
162        builder: CreateCommand,
163    ) -> Result<Command> {
164        builder.execute(cache_http, (None, None)).await
165    }
166
167    /// Override all global application commands.
168    ///
169    /// # Errors
170    ///
171    /// Returns the same errors as [`Self::create_global_command`].
172    pub async fn set_global_commands(
173        http: impl AsRef<Http>,
174        commands: Vec<CreateCommand>,
175    ) -> Result<Vec<Command>> {
176        http.as_ref().create_global_commands(&commands).await
177    }
178
179    /// Edit a global command, given its Id.
180    ///
181    /// # Errors
182    ///
183    /// See [`CreateCommand::execute`] for a list of possible errors.
184    pub async fn edit_global_command(
185        cache_http: impl CacheHttp,
186        command_id: CommandId,
187        builder: CreateCommand,
188    ) -> Result<Command> {
189        builder.execute(cache_http, (None, Some(command_id))).await
190    }
191
192    /// Gets all global commands.
193    ///
194    /// # Errors
195    ///
196    /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`].
197    pub async fn get_global_commands(http: impl AsRef<Http>) -> Result<Vec<Command>> {
198        http.as_ref().get_global_commands().await
199    }
200
201    /// Gets all global commands with localizations.
202    ///
203    /// # Errors
204    ///
205    /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`].
206    pub async fn get_global_commands_with_localizations(
207        http: impl AsRef<Http>,
208    ) -> Result<Vec<Command>> {
209        http.as_ref().get_global_commands_with_localizations().await
210    }
211
212    /// Gets a global command by its Id.
213    ///
214    /// # Errors
215    ///
216    /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`].
217    pub async fn get_global_command(
218        http: impl AsRef<Http>,
219        command_id: CommandId,
220    ) -> Result<Command> {
221        http.as_ref().get_global_command(command_id).await
222    }
223
224    /// Deletes a global command by its Id.
225    ///
226    /// # Errors
227    ///
228    /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`].
229    pub async fn delete_global_command(
230        http: impl AsRef<Http>,
231        command_id: CommandId,
232    ) -> Result<()> {
233        http.as_ref().delete_global_command(command_id).await
234    }
235}
236
237enum_number! {
238    /// The type of an application command.
239    ///
240    /// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types).
241    #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
242    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
243    #[serde(from = "u8", into = "u8")]
244    #[non_exhaustive]
245    pub enum CommandType {
246        ChatInput = 1,
247        User = 2,
248        Message = 3,
249        PrimaryEntryPoint = 4,
250        _ => Unknown(u8),
251    }
252}
253
254enum_number! {
255    /// Signifies how the invocation of a command of type [`PrimaryEntryPoint`] should be handled.
256    ///
257    /// [`PrimaryEntryPoint`]: CommandType::PrimaryEntryPoint
258    /// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-entry-point-command-handler-types)
259    #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
260    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
261    #[serde(from = "u8", into = "u8")]
262    #[non_exhaustive]
263    pub enum EntryPointHandlerType {
264        AppHandler = 1,
265        DiscordLaunchActivity = 2,
266        _ => Unknown(u8),
267    }
268}
269
270/// The parameters for an [`Command`].
271///
272/// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure).
273#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
274#[derive(Clone, Debug, Deserialize, Serialize)]
275#[non_exhaustive]
276pub struct CommandOption {
277    /// The option type.
278    #[serde(rename = "type")]
279    pub kind: CommandOptionType,
280    /// The option name.
281    pub name: String,
282    /// Localizations of the option name with locale as the key
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub name_localizations: Option<std::collections::HashMap<String, String>>,
285    /// The option description.
286    pub description: String,
287    /// Localizations of the option description with locale as the key
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub description_localizations: Option<std::collections::HashMap<String, String>>,
290    /// Whether the parameter is optional or required.
291    #[serde(default)]
292    pub required: bool,
293    /// Choices the user can pick from.
294    ///
295    /// **Note**: Only available for [`String`] and [`Integer`] [`CommandOptionType`].
296    ///
297    /// [`String`]: CommandOptionType::String
298    /// [`Integer`]: CommandOptionType::Integer
299    #[serde(default)]
300    pub choices: Vec<CommandOptionChoice>,
301    /// The nested options.
302    ///
303    /// **Note**: Only available for [`SubCommand`] or [`SubCommandGroup`].
304    ///
305    /// [`SubCommand`]: CommandOptionType::SubCommand
306    /// [`SubCommandGroup`]: CommandOptionType::SubCommandGroup
307    #[serde(default)]
308    pub options: Vec<CommandOption>,
309    /// If the option is a [`Channel`], it will only be able to show these types.
310    ///
311    /// [`Channel`]: CommandOptionType::Channel
312    #[serde(default)]
313    pub channel_types: Vec<ChannelType>,
314    /// Minimum permitted value for Integer or Number options
315    #[serde(default)]
316    pub min_value: Option<serde_json::Number>,
317    /// Maximum permitted value for Integer or Number options
318    #[serde(default)]
319    pub max_value: Option<serde_json::Number>,
320    /// Minimum permitted length for String options
321    #[serde(default)]
322    pub min_length: Option<u16>,
323    /// Maximum permitted length for String options
324    #[serde(default)]
325    pub max_length: Option<u16>,
326    #[serde(default)]
327    pub autocomplete: bool,
328}
329
330enum_number! {
331    /// The type of an [`CommandOption`].
332    ///
333    /// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type).
334    #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
335    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
336    #[serde(from = "u8", into = "u8")]
337    #[non_exhaustive]
338    pub enum CommandOptionType {
339        SubCommand = 1,
340        SubCommandGroup = 2,
341        String = 3,
342        Integer = 4,
343        Boolean = 5,
344        User = 6,
345        Channel = 7,
346        Role = 8,
347        Mentionable = 9,
348        Number = 10,
349        Attachment = 11,
350        _ => Unknown(u8),
351    }
352}
353
354/// The only valid values a user can pick in an [`CommandOption`].
355///
356/// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-choice-structure).
357#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
358#[derive(Clone, Debug, Deserialize, Serialize)]
359#[non_exhaustive]
360pub struct CommandOptionChoice {
361    /// The choice name.
362    pub name: String,
363    /// Localizations of the choice name, with locale as key
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub name_localizations: Option<std::collections::HashMap<String, String>>,
366    /// The choice value.
367    pub value: Value,
368}
369
370/// An [`Command`] permission.
371///
372/// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-guild-application-command-permissions-structure).
373#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
374#[derive(Clone, Debug, Deserialize, Serialize)]
375#[non_exhaustive]
376pub struct CommandPermissions {
377    /// The id of the command.
378    pub id: CommandId,
379    /// The id of the application the command belongs to.
380    pub application_id: ApplicationId,
381    /// The id of the guild.
382    pub guild_id: GuildId,
383    /// The permissions for the command in the guild.
384    pub permissions: Vec<CommandPermission>,
385}
386
387/// The [`CommandPermission`] data.
388///
389/// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure).
390#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
391#[derive(Clone, Debug, Deserialize, Serialize)]
392#[non_exhaustive]
393pub struct CommandPermission {
394    /// The [`RoleId`] or [`UserId`], depends on `kind` value.
395    pub id: CommandPermissionId,
396    /// The type of data this permissions applies to.
397    #[serde(rename = "type")]
398    pub kind: CommandPermissionType,
399    /// Whether or not the provided data can use the command or not.
400    pub permission: bool,
401}
402
403enum_number! {
404    /// The type of a [`CommandPermission`].
405    ///
406    /// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permission-type).
407    #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
408    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
409    #[serde(from = "u8", into = "u8")]
410    #[non_exhaustive]
411    pub enum CommandPermissionType {
412        Role = 1,
413        User = 2,
414        Channel = 3,
415        _ => Unknown(u8),
416    }
417}
418
419impl CommandPermissionId {
420    /// Converts this [`CommandPermissionId`] to [`UserId`].
421    #[must_use]
422    pub fn to_user_id(self) -> UserId {
423        self.into()
424    }
425
426    /// Converts this [`CommandPermissionId`] to [`RoleId`].
427    #[must_use]
428    pub fn to_role_id(self) -> RoleId {
429        self.into()
430    }
431}
432
433impl From<RoleId> for CommandPermissionId {
434    fn from(id: RoleId) -> Self {
435        Self::new(id.get())
436    }
437}
438
439impl From<UserId> for CommandPermissionId {
440    fn from(id: UserId) -> Self {
441        Self::new(id.get())
442    }
443}
444
445impl From<CommandPermissionId> for RoleId {
446    fn from(id: CommandPermissionId) -> Self {
447        Self::new(id.get())
448    }
449}
450
451impl From<CommandPermissionId> for UserId {
452    fn from(id: CommandPermissionId) -> Self {
453        Self::new(id.get())
454    }
455}