Skip to main content

twilight_model/application/command/
mod.rs

1//! Used for building commands to send to Discord.
2//!
3//! It is highly recommended to use the associated [`CommandBuilder`] in the
4//! [`twilight-util`] to create [`Command`]s; [`CommandOption`] is especially
5//! verbose.
6//!
7//! [`CommandBuilder`]: https://docs.rs/twilight-util/latest/twilight_util/builder/command/index.html
8//! [`twilight-util`]: https://docs.rs/twilight-util
9
10pub mod permissions;
11
12mod command_type;
13mod option;
14
15pub use self::{
16    command_type::CommandType,
17    option::{
18        CommandOption, CommandOptionChoice, CommandOptionChoiceValue, CommandOptionType,
19        CommandOptionValue,
20    },
21};
22
23use crate::{
24    guild::Permissions,
25    id::{
26        Id,
27        marker::{ApplicationMarker, CommandMarker, CommandVersionMarker, GuildMarker},
28    },
29    oauth::ApplicationIntegrationType,
30};
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33
34use super::interaction::InteractionContextType;
35
36/// Data sent to Discord to create a command.
37///
38/// [`CommandOption`]s that are required must be listed before optional ones.
39/// Command names must be lower case, matching the Regex `^[\w-]{1,32}$`. See
40/// [Discord Docs/Application Command Object].
41///
42/// [Discord Docs/Application Command Object]: https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure
43#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
44pub struct Command {
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub application_id: Option<Id<ApplicationMarker>>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub contexts: Option<Vec<InteractionContextType>>,
49    /// Default permissions required for a member to run the command.
50    ///
51    /// Setting this [`Permissions::empty()`] will prohibit anyone from running
52    /// the command, except for guild administrators.
53    pub default_member_permissions: Option<Permissions>,
54    /// Description of the command.
55    ///
56    /// For [`User`] and [`Message`] commands, this will be an empty string.
57    ///
58    /// [`User`]: CommandType::User
59    /// [`Message`]: CommandType::Message
60    pub description: String,
61    /// Localization dictionary for the `description` field.
62    ///
63    /// See [Discord Docs/Localization].
64    ///
65    /// [Discord Docs/Localization]: https://discord.com/developers/docs/interactions/application-commands#localization
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub description_localizations: Option<HashMap<String, String>>,
68    /// Whether the command is available in DMs.
69    ///
70    /// This is only relevant for globally-scoped commands. By default, commands
71    /// are visible in DMs.
72    #[deprecated(note = "use contexts instead")]
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub dm_permission: Option<bool>,
75    /// Guild ID of the command, if not global.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub guild_id: Option<Id<GuildMarker>>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub id: Option<Id<CommandMarker>>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub integration_types: Option<Vec<ApplicationIntegrationType>>,
82    #[serde(rename = "type")]
83    pub kind: CommandType,
84    pub name: String,
85    /// Localization dictionary for the `name` field.
86    ///
87    /// Keys should be valid locales. See [Discord Docs/Locales],
88    /// [Discord Docs/Localization].
89    ///
90    /// [Discord Docs/Locales]: https://discord.com/developers/docs/reference#locales
91    /// [Discord Docs/Localization]: https://discord.com/developers/docs/interactions/application-commands#localization
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub name_localizations: Option<HashMap<String, String>>,
94    /// Whether the command is age-restricted.
95    ///
96    /// Defaults to false.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub nsfw: Option<bool>,
99    #[serde(default)]
100    pub options: Vec<CommandOption>,
101    /// Autoincrementing version identifier.
102    pub version: Id<CommandVersionMarker>,
103}
104
105#[cfg(test)]
106mod tests {
107    use super::{
108        Command, CommandOption, CommandOptionChoice, CommandOptionChoiceValue, CommandOptionType,
109        CommandOptionValue, CommandType,
110    };
111    use crate::{channel::ChannelType, guild::Permissions, id::Id};
112    use serde_test::Token;
113    use std::collections::HashMap;
114
115    #[test]
116    #[allow(clippy::too_many_lines, deprecated)]
117    fn command_option_full() {
118        let value = Command {
119            application_id: Some(Id::new(100)),
120            contexts: None,
121            default_member_permissions: Some(Permissions::ADMINISTRATOR),
122            description: "this command is a test".into(),
123            description_localizations: Some(HashMap::from([(
124                "en-US".into(),
125                "this command is a test".into(),
126            )])),
127            dm_permission: Some(false),
128            guild_id: Some(Id::new(300)),
129            id: Some(Id::new(200)),
130            integration_types: None,
131            kind: CommandType::ChatInput,
132            name: "test command".into(),
133            name_localizations: Some(HashMap::from([("en-US".into(), "test command".into())])),
134            nsfw: None,
135            options: Vec::from([CommandOption {
136                autocomplete: None,
137                channel_types: None,
138                choices: None,
139                description: "sub command group desc".to_owned(),
140                description_localizations: None,
141                kind: CommandOptionType::SubCommandGroup,
142                max_length: None,
143                max_value: None,
144                min_length: None,
145                min_value: None,
146                name: "sub command group name".to_owned(),
147                name_localizations: None,
148                options: Some(Vec::from([CommandOption {
149                    autocomplete: None,
150                    channel_types: None,
151                    choices: None,
152                    description: "sub command desc".to_owned(),
153                    description_localizations: None,
154                    kind: CommandOptionType::SubCommand,
155                    max_length: None,
156                    max_value: None,
157                    min_length: None,
158                    min_value: None,
159                    name: "sub command name".to_owned(),
160                    name_localizations: None,
161                    options: Some(Vec::from([
162                        CommandOption {
163                            autocomplete: None,
164                            channel_types: None,
165                            choices: None,
166                            description: "attachment desc".to_owned(),
167                            description_localizations: None,
168                            kind: CommandOptionType::Attachment,
169                            max_length: None,
170                            max_value: None,
171                            min_length: None,
172                            min_value: None,
173                            name: "attachment name".to_owned(),
174                            name_localizations: None,
175                            options: None,
176                            file_types: None,
177                            required: None,
178                        },
179                        CommandOption {
180                            autocomplete: None,
181                            channel_types: None,
182                            choices: None,
183                            description: "boolean desc".to_owned(),
184                            description_localizations: None,
185                            kind: CommandOptionType::Boolean,
186                            max_length: None,
187                            max_value: None,
188                            min_length: None,
189                            min_value: None,
190                            name: "boolean name".to_owned(),
191                            name_localizations: None,
192                            options: None,
193                            file_types: None,
194                            required: Some(true),
195                        },
196                        CommandOption {
197                            autocomplete: None,
198                            channel_types: Some(Vec::new()),
199                            choices: None,
200                            description: "channel desc".to_owned(),
201                            description_localizations: None,
202                            kind: CommandOptionType::Channel,
203                            max_length: None,
204                            max_value: None,
205                            min_length: None,
206                            min_value: None,
207                            name: "channel name".to_owned(),
208                            name_localizations: None,
209                            options: None,
210                            file_types: None,
211                            required: None,
212                        },
213                        CommandOption {
214                            autocomplete: None,
215                            channel_types: Some(Vec::from([ChannelType::GuildText])),
216                            choices: None,
217                            description: "channel desc".to_owned(),
218                            description_localizations: None,
219                            kind: CommandOptionType::Channel,
220                            max_length: None,
221                            max_value: None,
222                            min_length: None,
223                            min_value: None,
224                            name: "channel name".to_owned(),
225                            name_localizations: None,
226                            options: None,
227                            file_types: None,
228                            required: None,
229                        },
230                        CommandOption {
231                            autocomplete: Some(true),
232                            channel_types: None,
233                            choices: Some(Vec::new()),
234                            description: "integer desc".to_owned(),
235                            description_localizations: None,
236                            kind: CommandOptionType::Integer,
237                            max_length: None,
238                            max_value: Some(CommandOptionValue::Integer(100)),
239                            min_length: None,
240                            min_value: Some(CommandOptionValue::Integer(0)),
241                            name: "integer name".to_owned(),
242                            name_localizations: None,
243                            options: None,
244                            file_types: None,
245                            required: None,
246                        },
247                        CommandOption {
248                            autocomplete: None,
249                            channel_types: None,
250                            choices: None,
251                            description: "mentionable desc".to_owned(),
252                            description_localizations: Some(HashMap::from([(
253                                "en-GB".to_owned(),
254                                "mentionable desc (but british)".to_owned(),
255                            )])),
256                            kind: CommandOptionType::Mentionable,
257                            max_length: None,
258                            max_value: None,
259                            min_length: None,
260                            min_value: None,
261                            name: "mentionable name".to_owned(),
262                            name_localizations: None,
263                            options: None,
264                            file_types: None,
265                            required: None,
266                        },
267                        CommandOption {
268                            autocomplete: Some(false),
269                            channel_types: None,
270                            choices: Some(Vec::from([CommandOptionChoice {
271                                name: "number choice".to_owned(),
272                                name_localizations: Some(HashMap::from([(
273                                    "en-US".to_owned(),
274                                    "number choice (but american)".to_owned(),
275                                )])),
276                                value: CommandOptionChoiceValue::Number(10.0),
277                            }])),
278                            description: "number desc".to_owned(),
279                            description_localizations: None,
280                            kind: CommandOptionType::Number,
281                            max_length: None,
282                            max_value: None,
283                            min_length: None,
284                            min_value: None,
285                            name: "number name".to_owned(),
286                            name_localizations: None,
287                            options: None,
288                            file_types: None,
289                            required: None,
290                        },
291                        CommandOption {
292                            autocomplete: None,
293                            channel_types: None,
294                            choices: None,
295                            description: "role desc".to_owned(),
296                            description_localizations: None,
297                            kind: CommandOptionType::Role,
298                            max_length: None,
299                            max_value: None,
300                            min_length: None,
301                            min_value: None,
302                            name: "role name".to_owned(),
303                            name_localizations: Some(HashMap::from([(
304                                "de-DE".to_owned(),
305                                "role name (but german)".to_owned(),
306                            )])),
307                            options: None,
308                            file_types: None,
309                            required: None,
310                        },
311                        CommandOption {
312                            autocomplete: None,
313                            channel_types: None,
314                            choices: None,
315                            description: "string desc".to_owned(),
316                            description_localizations: None,
317                            kind: CommandOptionType::String,
318                            max_length: Some(6000),
319                            max_value: None,
320                            min_length: Some(0),
321                            min_value: None,
322                            name: "string name".to_owned(),
323                            name_localizations: None,
324                            options: None,
325                            file_types: None,
326                            required: None,
327                        },
328                    ])),
329                    file_types: None,
330                    required: None,
331                }])),
332                file_types: None,
333                required: None,
334            }]),
335            version: Id::new(1),
336        };
337
338        serde_test::assert_tokens(
339            &value,
340            &[
341                Token::Struct {
342                    name: "Command",
343                    len: 12,
344                },
345                Token::Str("application_id"),
346                Token::Some,
347                Token::NewtypeStruct { name: "Id" },
348                Token::Str("100"),
349                Token::Str("default_member_permissions"),
350                Token::Some,
351                Token::Str("8"),
352                Token::Str("description"),
353                Token::Str("this command is a test"),
354                Token::Str("description_localizations"),
355                Token::Some,
356                Token::Map { len: Some(1) },
357                Token::Str("en-US"),
358                Token::Str("this command is a test"),
359                Token::MapEnd,
360                Token::Str("dm_permission"),
361                Token::Some,
362                Token::Bool(false),
363                Token::Str("guild_id"),
364                Token::Some,
365                Token::NewtypeStruct { name: "Id" },
366                Token::Str("300"),
367                Token::Str("id"),
368                Token::Some,
369                Token::NewtypeStruct { name: "Id" },
370                Token::Str("200"),
371                Token::Str("type"),
372                Token::U8(CommandType::ChatInput.into()),
373                Token::Str("name"),
374                Token::Str("test command"),
375                Token::Str("name_localizations"),
376                Token::Some,
377                Token::Map { len: Some(1) },
378                Token::Str("en-US"),
379                Token::Str("test command"),
380                Token::MapEnd,
381                Token::Str("options"),
382                Token::Seq { len: Some(1) },
383                Token::Struct {
384                    name: "CommandOption",
385                    len: 4,
386                },
387                Token::Str("description"),
388                Token::Str("sub command group desc"),
389                Token::Str("type"),
390                Token::U8(CommandOptionType::SubCommandGroup as u8),
391                Token::Str("name"),
392                Token::Str("sub command group name"),
393                Token::Str("options"),
394                Token::Some,
395                Token::Seq { len: Some(1) },
396                Token::Struct {
397                    name: "CommandOption",
398                    len: 4,
399                },
400                Token::Str("description"),
401                Token::Str("sub command desc"),
402                Token::Str("type"),
403                Token::U8(CommandOptionType::SubCommand as u8),
404                Token::Str("name"),
405                Token::Str("sub command name"),
406                Token::Str("options"),
407                Token::Some,
408                Token::Seq { len: Some(9) },
409                Token::Struct {
410                    name: "CommandOption",
411                    len: 3,
412                },
413                Token::Str("description"),
414                Token::Str("attachment desc"),
415                Token::Str("type"),
416                Token::U8(CommandOptionType::Attachment as u8),
417                Token::Str("name"),
418                Token::Str("attachment name"),
419                Token::StructEnd,
420                Token::Struct {
421                    name: "CommandOption",
422                    len: 4,
423                },
424                Token::Str("description"),
425                Token::Str("boolean desc"),
426                Token::Str("type"),
427                Token::U8(CommandOptionType::Boolean as u8),
428                Token::Str("name"),
429                Token::Str("boolean name"),
430                Token::Str("required"),
431                Token::Some,
432                Token::Bool(true),
433                Token::StructEnd,
434                Token::Struct {
435                    name: "CommandOption",
436                    len: 4,
437                },
438                Token::Str("channel_types"),
439                Token::Some,
440                Token::Seq { len: Some(0) },
441                Token::SeqEnd,
442                Token::Str("description"),
443                Token::Str("channel desc"),
444                Token::Str("type"),
445                Token::U8(CommandOptionType::Channel as u8),
446                Token::Str("name"),
447                Token::Str("channel name"),
448                Token::StructEnd,
449                Token::Struct {
450                    name: "CommandOption",
451                    len: 4,
452                },
453                Token::Str("channel_types"),
454                Token::Some,
455                Token::Seq { len: Some(1) },
456                Token::U8(ChannelType::GuildText.into()),
457                Token::SeqEnd,
458                Token::Str("description"),
459                Token::Str("channel desc"),
460                Token::Str("type"),
461                Token::U8(CommandOptionType::Channel as u8),
462                Token::Str("name"),
463                Token::Str("channel name"),
464                Token::StructEnd,
465                Token::Struct {
466                    name: "CommandOption",
467                    len: 7,
468                },
469                Token::Str("autocomplete"),
470                Token::Some,
471                Token::Bool(true),
472                Token::Str("choices"),
473                Token::Some,
474                Token::Seq { len: Some(0) },
475                Token::SeqEnd,
476                Token::Str("description"),
477                Token::Str("integer desc"),
478                Token::Str("type"),
479                Token::U8(CommandOptionType::Integer as u8),
480                Token::Str("max_value"),
481                Token::Some,
482                Token::I64(100),
483                Token::Str("min_value"),
484                Token::Some,
485                Token::I64(0),
486                Token::Str("name"),
487                Token::Str("integer name"),
488                Token::StructEnd,
489                Token::Struct {
490                    name: "CommandOption",
491                    len: 4,
492                },
493                Token::Str("description"),
494                Token::Str("mentionable desc"),
495                Token::Str("description_localizations"),
496                Token::Some,
497                Token::Map { len: Some(1) },
498                Token::Str("en-GB"),
499                Token::Str("mentionable desc (but british)"),
500                Token::MapEnd,
501                Token::Str("type"),
502                Token::U8(CommandOptionType::Mentionable as u8),
503                Token::Str("name"),
504                Token::Str("mentionable name"),
505                Token::StructEnd,
506                Token::Struct {
507                    name: "CommandOption",
508                    len: 5,
509                },
510                Token::Str("autocomplete"),
511                Token::Some,
512                Token::Bool(false),
513                Token::Str("choices"),
514                Token::Some,
515                Token::Seq { len: Some(1) },
516                Token::Struct {
517                    name: "CommandOptionChoice",
518                    len: 3,
519                },
520                Token::Str("name"),
521                Token::Str("number choice"),
522                Token::Str("name_localizations"),
523                Token::Some,
524                Token::Map { len: Some(1) },
525                Token::Str("en-US"),
526                Token::Str("number choice (but american)"),
527                Token::MapEnd,
528                Token::Str("value"),
529                Token::F64(10.0),
530                Token::StructEnd,
531                Token::SeqEnd,
532                Token::Str("description"),
533                Token::Str("number desc"),
534                Token::Str("type"),
535                Token::U8(CommandOptionType::Number as u8),
536                Token::Str("name"),
537                Token::Str("number name"),
538                Token::StructEnd,
539                Token::Struct {
540                    name: "CommandOption",
541                    len: 4,
542                },
543                Token::Str("description"),
544                Token::Str("role desc"),
545                Token::Str("type"),
546                Token::U8(CommandOptionType::Role as u8),
547                Token::Str("name"),
548                Token::Str("role name"),
549                Token::Str("name_localizations"),
550                Token::Some,
551                Token::Map { len: Some(1) },
552                Token::Str("de-DE"),
553                Token::Str("role name (but german)"),
554                Token::MapEnd,
555                Token::StructEnd,
556                Token::Struct {
557                    name: "CommandOption",
558                    len: 5,
559                },
560                Token::Str("description"),
561                Token::Str("string desc"),
562                Token::Str("type"),
563                Token::U8(CommandOptionType::String as u8),
564                Token::Str("max_length"),
565                Token::Some,
566                Token::U16(6000),
567                Token::Str("min_length"),
568                Token::Some,
569                Token::U16(0),
570                Token::Str("name"),
571                Token::Str("string name"),
572                Token::StructEnd,
573                Token::SeqEnd,
574                Token::StructEnd,
575                Token::SeqEnd,
576                Token::StructEnd,
577                Token::SeqEnd,
578                Token::Str("version"),
579                Token::NewtypeStruct { name: "Id" },
580                Token::Str("1"),
581                Token::StructEnd,
582            ],
583        );
584    }
585}