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        marker::{ApplicationMarker, CommandMarker, CommandVersionMarker, GuildMarker},
27        Id,
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    /// Whether the command is available in DMs.
55    ///
56    /// This is only relevant for globally-scoped commands. By default, commands
57    /// are visible in DMs.
58    #[deprecated(note = "use contexts instead")]
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub dm_permission: Option<bool>,
61    /// Description of the command.
62    ///
63    /// For [`User`] and [`Message`] commands, this will be an empty string.
64    ///
65    /// [`User`]: CommandType::User
66    /// [`Message`]: CommandType::Message
67    pub description: String,
68    /// Localization dictionary for the `description` field.
69    ///
70    /// See [Discord Docs/Localization].
71    ///
72    /// [Discord Docs/Localization]: https://discord.com/developers/docs/interactions/application-commands#localization
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub description_localizations: Option<HashMap<String, String>>,
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            dm_permission: Some(false),
123            description: "this command is a test".into(),
124            description_localizations: Some(HashMap::from([(
125                "en-US".into(),
126                "this command is a test".into(),
127            )])),
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                            required: None,
177                        },
178                        CommandOption {
179                            autocomplete: None,
180                            channel_types: None,
181                            choices: None,
182                            description: "boolean desc".to_owned(),
183                            description_localizations: None,
184                            kind: CommandOptionType::Boolean,
185                            max_length: None,
186                            max_value: None,
187                            min_length: None,
188                            min_value: None,
189                            name: "boolean name".to_owned(),
190                            name_localizations: None,
191                            options: None,
192                            required: Some(true),
193                        },
194                        CommandOption {
195                            autocomplete: None,
196                            channel_types: Some(Vec::new()),
197                            choices: None,
198                            description: "channel desc".to_owned(),
199                            description_localizations: None,
200                            kind: CommandOptionType::Channel,
201                            max_length: None,
202                            max_value: None,
203                            min_length: None,
204                            min_value: None,
205                            name: "channel name".to_owned(),
206                            name_localizations: None,
207                            options: None,
208                            required: None,
209                        },
210                        CommandOption {
211                            autocomplete: None,
212                            channel_types: Some(Vec::from([ChannelType::GuildText])),
213                            choices: None,
214                            description: "channel desc".to_owned(),
215                            description_localizations: None,
216                            kind: CommandOptionType::Channel,
217                            max_length: None,
218                            max_value: None,
219                            min_length: None,
220                            min_value: None,
221                            name: "channel name".to_owned(),
222                            name_localizations: None,
223                            options: None,
224                            required: None,
225                        },
226                        CommandOption {
227                            autocomplete: Some(true),
228                            channel_types: None,
229                            choices: Some(Vec::new()),
230                            description: "integer desc".to_owned(),
231                            description_localizations: None,
232                            kind: CommandOptionType::Integer,
233                            max_length: None,
234                            max_value: Some(CommandOptionValue::Integer(100)),
235                            min_length: None,
236                            min_value: Some(CommandOptionValue::Integer(0)),
237                            name: "integer name".to_owned(),
238                            name_localizations: None,
239                            options: None,
240                            required: None,
241                        },
242                        CommandOption {
243                            autocomplete: None,
244                            channel_types: None,
245                            choices: None,
246                            description: "mentionable desc".to_owned(),
247                            description_localizations: Some(HashMap::from([(
248                                "en-GB".to_owned(),
249                                "mentionable desc (but british)".to_owned(),
250                            )])),
251                            kind: CommandOptionType::Mentionable,
252                            max_length: None,
253                            max_value: None,
254                            min_length: None,
255                            min_value: None,
256                            name: "mentionable name".to_owned(),
257                            name_localizations: None,
258                            options: None,
259                            required: None,
260                        },
261                        CommandOption {
262                            autocomplete: Some(false),
263                            channel_types: None,
264                            choices: Some(Vec::from([CommandOptionChoice {
265                                name: "number choice".to_owned(),
266                                name_localizations: Some(HashMap::from([(
267                                    "en-US".to_owned(),
268                                    "number choice (but american)".to_owned(),
269                                )])),
270                                value: CommandOptionChoiceValue::Number(10.0),
271                            }])),
272                            description: "number desc".to_owned(),
273                            description_localizations: None,
274                            kind: CommandOptionType::Number,
275                            max_length: None,
276                            max_value: None,
277                            min_length: None,
278                            min_value: None,
279                            name: "number name".to_owned(),
280                            name_localizations: None,
281                            options: None,
282                            required: None,
283                        },
284                        CommandOption {
285                            autocomplete: None,
286                            channel_types: None,
287                            choices: None,
288                            description: "role desc".to_owned(),
289                            description_localizations: None,
290                            kind: CommandOptionType::Role,
291                            max_length: None,
292                            max_value: None,
293                            min_length: None,
294                            min_value: None,
295                            name: "role name".to_owned(),
296                            name_localizations: Some(HashMap::from([(
297                                "de-DE".to_owned(),
298                                "role name (but german)".to_owned(),
299                            )])),
300                            options: None,
301                            required: None,
302                        },
303                        CommandOption {
304                            autocomplete: None,
305                            channel_types: None,
306                            choices: None,
307                            description: "string desc".to_owned(),
308                            description_localizations: None,
309                            kind: CommandOptionType::String,
310                            max_length: Some(6000),
311                            max_value: None,
312                            min_length: Some(0),
313                            min_value: None,
314                            name: "string name".to_owned(),
315                            name_localizations: None,
316                            options: None,
317                            required: None,
318                        },
319                    ])),
320                    required: None,
321                }])),
322                required: None,
323            }]),
324            version: Id::new(1),
325        };
326
327        serde_test::assert_tokens(
328            &value,
329            &[
330                Token::Struct {
331                    name: "Command",
332                    len: 12,
333                },
334                Token::Str("application_id"),
335                Token::Some,
336                Token::NewtypeStruct { name: "Id" },
337                Token::Str("100"),
338                Token::Str("default_member_permissions"),
339                Token::Some,
340                Token::Str("8"),
341                Token::Str("dm_permission"),
342                Token::Some,
343                Token::Bool(false),
344                Token::Str("description"),
345                Token::Str("this command is a test"),
346                Token::Str("description_localizations"),
347                Token::Some,
348                Token::Map { len: Some(1) },
349                Token::Str("en-US"),
350                Token::Str("this command is a test"),
351                Token::MapEnd,
352                Token::Str("guild_id"),
353                Token::Some,
354                Token::NewtypeStruct { name: "Id" },
355                Token::Str("300"),
356                Token::Str("id"),
357                Token::Some,
358                Token::NewtypeStruct { name: "Id" },
359                Token::Str("200"),
360                Token::Str("type"),
361                Token::U8(CommandType::ChatInput.into()),
362                Token::Str("name"),
363                Token::Str("test command"),
364                Token::Str("name_localizations"),
365                Token::Some,
366                Token::Map { len: Some(1) },
367                Token::Str("en-US"),
368                Token::Str("test command"),
369                Token::MapEnd,
370                Token::Str("options"),
371                Token::Seq { len: Some(1) },
372                Token::Struct {
373                    name: "CommandOption",
374                    len: 4,
375                },
376                Token::Str("description"),
377                Token::Str("sub command group desc"),
378                Token::Str("type"),
379                Token::U8(CommandOptionType::SubCommandGroup as u8),
380                Token::Str("name"),
381                Token::Str("sub command group name"),
382                Token::Str("options"),
383                Token::Some,
384                Token::Seq { len: Some(1) },
385                Token::Struct {
386                    name: "CommandOption",
387                    len: 4,
388                },
389                Token::Str("description"),
390                Token::Str("sub command desc"),
391                Token::Str("type"),
392                Token::U8(CommandOptionType::SubCommand as u8),
393                Token::Str("name"),
394                Token::Str("sub command name"),
395                Token::Str("options"),
396                Token::Some,
397                Token::Seq { len: Some(9) },
398                Token::Struct {
399                    name: "CommandOption",
400                    len: 3,
401                },
402                Token::Str("description"),
403                Token::Str("attachment desc"),
404                Token::Str("type"),
405                Token::U8(CommandOptionType::Attachment as u8),
406                Token::Str("name"),
407                Token::Str("attachment name"),
408                Token::StructEnd,
409                Token::Struct {
410                    name: "CommandOption",
411                    len: 4,
412                },
413                Token::Str("description"),
414                Token::Str("boolean desc"),
415                Token::Str("type"),
416                Token::U8(CommandOptionType::Boolean as u8),
417                Token::Str("name"),
418                Token::Str("boolean name"),
419                Token::Str("required"),
420                Token::Some,
421                Token::Bool(true),
422                Token::StructEnd,
423                Token::Struct {
424                    name: "CommandOption",
425                    len: 4,
426                },
427                Token::Str("channel_types"),
428                Token::Some,
429                Token::Seq { len: Some(0) },
430                Token::SeqEnd,
431                Token::Str("description"),
432                Token::Str("channel desc"),
433                Token::Str("type"),
434                Token::U8(CommandOptionType::Channel as u8),
435                Token::Str("name"),
436                Token::Str("channel name"),
437                Token::StructEnd,
438                Token::Struct {
439                    name: "CommandOption",
440                    len: 4,
441                },
442                Token::Str("channel_types"),
443                Token::Some,
444                Token::Seq { len: Some(1) },
445                Token::U8(ChannelType::GuildText.into()),
446                Token::SeqEnd,
447                Token::Str("description"),
448                Token::Str("channel desc"),
449                Token::Str("type"),
450                Token::U8(CommandOptionType::Channel as u8),
451                Token::Str("name"),
452                Token::Str("channel name"),
453                Token::StructEnd,
454                Token::Struct {
455                    name: "CommandOption",
456                    len: 7,
457                },
458                Token::Str("autocomplete"),
459                Token::Some,
460                Token::Bool(true),
461                Token::Str("choices"),
462                Token::Some,
463                Token::Seq { len: Some(0) },
464                Token::SeqEnd,
465                Token::Str("description"),
466                Token::Str("integer desc"),
467                Token::Str("type"),
468                Token::U8(CommandOptionType::Integer as u8),
469                Token::Str("max_value"),
470                Token::Some,
471                Token::I64(100),
472                Token::Str("min_value"),
473                Token::Some,
474                Token::I64(0),
475                Token::Str("name"),
476                Token::Str("integer name"),
477                Token::StructEnd,
478                Token::Struct {
479                    name: "CommandOption",
480                    len: 4,
481                },
482                Token::Str("description"),
483                Token::Str("mentionable desc"),
484                Token::Str("description_localizations"),
485                Token::Some,
486                Token::Map { len: Some(1) },
487                Token::Str("en-GB"),
488                Token::Str("mentionable desc (but british)"),
489                Token::MapEnd,
490                Token::Str("type"),
491                Token::U8(CommandOptionType::Mentionable as u8),
492                Token::Str("name"),
493                Token::Str("mentionable name"),
494                Token::StructEnd,
495                Token::Struct {
496                    name: "CommandOption",
497                    len: 5,
498                },
499                Token::Str("autocomplete"),
500                Token::Some,
501                Token::Bool(false),
502                Token::Str("choices"),
503                Token::Some,
504                Token::Seq { len: Some(1) },
505                Token::Struct {
506                    name: "CommandOptionChoice",
507                    len: 3,
508                },
509                Token::Str("name"),
510                Token::Str("number choice"),
511                Token::Str("name_localizations"),
512                Token::Some,
513                Token::Map { len: Some(1) },
514                Token::Str("en-US"),
515                Token::Str("number choice (but american)"),
516                Token::MapEnd,
517                Token::Str("value"),
518                Token::F64(10.0),
519                Token::StructEnd,
520                Token::SeqEnd,
521                Token::Str("description"),
522                Token::Str("number desc"),
523                Token::Str("type"),
524                Token::U8(CommandOptionType::Number as u8),
525                Token::Str("name"),
526                Token::Str("number name"),
527                Token::StructEnd,
528                Token::Struct {
529                    name: "CommandOption",
530                    len: 4,
531                },
532                Token::Str("description"),
533                Token::Str("role desc"),
534                Token::Str("type"),
535                Token::U8(CommandOptionType::Role as u8),
536                Token::Str("name"),
537                Token::Str("role name"),
538                Token::Str("name_localizations"),
539                Token::Some,
540                Token::Map { len: Some(1) },
541                Token::Str("de-DE"),
542                Token::Str("role name (but german)"),
543                Token::MapEnd,
544                Token::StructEnd,
545                Token::Struct {
546                    name: "CommandOption",
547                    len: 5,
548                },
549                Token::Str("description"),
550                Token::Str("string desc"),
551                Token::Str("type"),
552                Token::U8(CommandOptionType::String as u8),
553                Token::Str("max_length"),
554                Token::Some,
555                Token::U16(6000),
556                Token::Str("min_length"),
557                Token::Some,
558                Token::U16(0),
559                Token::Str("name"),
560                Token::Str("string name"),
561                Token::StructEnd,
562                Token::SeqEnd,
563                Token::StructEnd,
564                Token::SeqEnd,
565                Token::StructEnd,
566                Token::SeqEnd,
567                Token::Str("version"),
568                Token::NewtypeStruct { name: "Id" },
569                Token::Str("1"),
570                Token::StructEnd,
571            ],
572        );
573    }
574}