Skip to main content

twilight_interactions/command/
command_model.rs

1use std::borrow::Cow;
2
3use twilight_model::{
4    application::{
5        command::CommandOptionValue as NumberCommandOptionValue,
6        interaction::{
7            application_command::{CommandData, CommandDataOption, CommandOptionValue},
8            InteractionChannel, InteractionDataResolved, InteractionMember,
9        },
10    },
11    channel::Attachment,
12    guild::Role,
13    id::{
14        marker::{AttachmentMarker, ChannelMarker, GenericMarker, RoleMarker, UserMarker},
15        Id,
16    },
17    user::User,
18};
19
20use super::internal::CommandOptionData;
21use crate::error::{ParseError, ParseOptionError, ParseOptionErrorType};
22
23/// Parse command data into a concrete type.
24///
25/// This trait is used to parse received command data into a concrete
26/// command model. A derive macro is provided to implement this trait
27/// automatically.
28///
29/// ## Command models
30/// This trait can be implemented on structs representing a slash command
31/// model. All type fields must implement the [`CommandOption`] trait. A
32/// unit struct can be used if the command has no options. See the
33/// [module documentation](crate::command) for a full list of supported types.
34///
35/// ```
36/// use twilight_interactions::command::{CommandModel, ResolvedUser};
37///
38/// #[derive(CommandModel)]
39/// struct HelloCommand {
40///     message: String,
41///     user: Option<ResolvedUser>,
42/// }
43/// ```
44///
45/// ### Validating options
46/// The [`CommandModel`] trait only focuses on parsing received interaction data
47/// and does not directly support additional validation. However, it will ensure
48/// that received data matches with the provided model. If you specify a
49/// `max_value` for a field, this requirement will be checked when parsing
50/// command data.
51///
52/// Not supporting additional validation is a design choice. This allows
53/// splitting validations that are ensured by Discord and those you perform
54/// on top of them. If an error occurs during parsing, it is always a bug, not
55/// a user mistake.
56///
57/// If you need to perform additional validation, consider creating another type
58/// that can be initialized from the command model.
59///
60/// ### Autocomplete interactions
61/// Autocomplete interactions are supported with the `#[command(autocomplete = true)]`
62/// attribute. Only autocomplete command models are able to use the [`AutocompleteValue`]
63/// type in command fields.
64///
65/// Since autocomplete interactions are partial interactions, models must meet
66/// the following requirements:
67/// - Every field should be an [`Option<T>`] or [`AutocompleteValue<T>`], since
68///   there is no guarantee that a specific field has been filled before the
69///   interaction is submitted.
70/// - If a field has autocomplete enabled, its type must be [`AutocompleteValue`]
71///   or the parsing will fail, since focused fields are sent as [`String`].
72/// - Autocomplete models are **partial**, which means that unknown fields
73///   will not make the parsing fail.
74/// - It is not possible to derive [`CreateCommand`] on autocomplete models.
75///
76/// <div class="warning">
77///
78/// Autocomplete models are not meant to be used alone: you should use a regular
79/// model to handle interactions submit, and another for autocomplete interactions.
80///
81/// </div>
82///
83/// ```
84/// use twilight_interactions::command::{AutocompleteValue, CommandModel, ResolvedUser};
85///
86/// #[derive(CommandModel)]
87/// #[command(autocomplete = true)]
88/// struct HelloCommand {
89///     message: AutocompleteValue<String>,
90///     user: Option<ResolvedUser>,
91/// }
92/// ```
93///
94/// ## Subcommands and subcommands groups
95/// This trait also supports parsing subcommands and subcommand groups when
96/// implemented on enums with all variants containing types that implement
97/// [`CommandModel`]. Each variant must have an attribute with the subcommand
98/// name.
99///
100/// Subcommand groups work the same way as regular subcommands, except the
101/// variant type is another enum implementing [`CommandModel`].
102///
103/// <div class="warning">
104///
105/// When using subcommands, you should parse and create the command using the
106/// top-level command. See the [`xkcd-bot` example] for example usage.
107///
108/// [`xkcd-bot` example]: https://github.com/baptiste0928/twilight-interactions/tree/main/examples/xkcd-bot
109///
110/// </div>
111///
112/// ```
113/// use twilight_interactions::command::CommandModel;
114/// #
115/// # #[derive(CommandModel)]
116/// # struct HelloUser {
117/// #    message: String,
118/// # }
119/// #
120/// # #[derive(CommandModel)]
121/// # struct HelloConfig {
122/// #    message: String,
123/// # }
124///
125/// #[derive(CommandModel)]
126/// enum HelloCommand {
127///     #[command(name = "user")]
128///     User(HelloUser),
129///     #[command(name = "config")]
130///     Config(HelloConfig),
131/// }
132/// ```
133///
134///
135/// ## Macro attributes
136/// The macro provides a `#[command]` attribute to configure generated code.
137///
138/// | Attribute                  | Type           | Location             | Description                                                     |
139/// |----------------------------|----------------|----------------------|-----------------------------------------------------------------|
140/// | `name`                     | `str`          | Variant (subcommand) | Subcommand name (required).                                     |
141/// | `rename`                   | `str`          | Field                | Use a different name for the field when parsing.                |
142/// | `channel_types`            | `str`          | Field                | Restricts the channel choice to specific types.[^channel_types] |
143/// | `max_value`, `min_value`   | `i64` or `f64` | Field                | Maximum and/or minimum value permitted.                         |
144/// | `max_length`, `min_length` | `u16`          | Field                | Maximum and/or minimum string length permitted.                 |
145///
146/// ### Example
147/// ```
148/// use twilight_interactions::command::CommandModel;
149///
150/// #[derive(CommandModel)]
151/// struct HelloCommand {
152///     #[command(rename = "text")]
153///     message: String,
154///     #[command(max_value = 60)]
155///     delay: i64,
156/// }
157/// ```
158///
159/// [^channel_types]: List of [`ChannelType`] names in snake_case separated by spaces
160///                   like `guild_text private`.
161///
162/// [`CreateCommand`]: super::CreateCommand
163/// [`ChannelType`]: twilight_model::channel::ChannelType
164pub trait CommandModel: Sized {
165    /// Construct this type from [`CommandInputData`].
166    fn from_interaction(data: CommandInputData) -> Result<Self, ParseError>;
167}
168
169impl<T: CommandModel> CommandModel for Box<T> {
170    fn from_interaction(data: CommandInputData) -> Result<Self, ParseError> {
171        T::from_interaction(data).map(Box::new)
172    }
173}
174
175impl CommandModel for Vec<CommandDataOption> {
176    fn from_interaction(data: CommandInputData) -> Result<Self, ParseError> {
177        Ok(data.options)
178    }
179}
180
181/// Parse command option into a concrete type.
182///
183/// This trait is used by the implementation of [`CommandModel`] generated
184/// by the derive macro. See the [module documentation](crate::command) for
185/// a list of implemented types.
186///
187/// ## Option choices
188/// This trait can be derived on enums to represent command options with
189/// predefined choices. The `#[option]` attribute must be present on each
190/// variant.
191///
192/// The corresponding slash command types are automatically inferred from
193/// the `value` attribute. In the example below, the inferred type would
194/// be `INTEGER`.
195///
196/// A `value` method is also generated for each variant to obtain the
197/// value of the variant. This method is not described in the trait
198/// as it is only implemented for option choices.
199///
200/// ### Example
201/// ```
202/// use twilight_interactions::command::CommandOption;
203///
204/// #[derive(CommandOption)]
205/// enum TimeUnit {
206///     #[option(name = "Minute", value = 60)]
207///     Minute,
208///     #[option(name = "Hour", value = 3600)]
209///     Hour,
210///     #[option(name = "Day", value = 86400)]
211///     Day,
212/// }
213///
214/// assert_eq!(TimeUnit::Minute.value(), 60);
215/// ```
216///
217/// ### Macro attributes
218/// The macro provides an `#[option]` attribute to configure the generated code.
219///
220/// | Attribute | Type                  | Location | Description                                |
221/// |-----------|-----------------------|----------|--------------------------------------------|
222/// | `name`    | `str`                 | Variant  | Set the name of the command option choice. |
223/// | `value`   | `str`, `i64` or `f64` | Variant  | Value of the command option choice.        |
224///
225pub trait CommandOption: Sized {
226    /// Convert a [`CommandOptionValue`] into this value.
227    fn from_option(
228        value: CommandOptionValue,
229        data: CommandOptionData,
230        resolved: Option<&InteractionDataResolved>,
231    ) -> Result<Self, ParseOptionErrorType>;
232}
233
234/// Data sent by Discord when receiving a command.
235///
236/// This type is used in the [`CommandModel`] trait. It can be initialized
237/// from [`CommandData`] using the [From] trait.
238///
239/// [`CommandModel`]: super::CommandModel
240#[derive(Debug, Clone, PartialEq)]
241pub struct CommandInputData<'a> {
242    pub options: Vec<CommandDataOption>,
243    pub resolved: Option<Cow<'a, InteractionDataResolved>>,
244}
245
246impl<'a> CommandInputData<'a> {
247    /// Parse a field from the command data.
248    ///
249    /// This method can be used to manually parse a field from
250    /// raw data, for example with guild custom commands. The
251    /// method returns [`None`] if the field is not present instead
252    /// of returning an error.
253    ///
254    /// ### Example
255    /// ```
256    /// use twilight_interactions::command::CommandInputData;
257    /// # use twilight_model::application::interaction::application_command::{CommandDataOption, CommandOptionValue};
258    /// #
259    /// # let options = vec![CommandDataOption { name: "message".into(), value: CommandOptionValue::String("Hello world".into()) }];
260    ///
261    /// // `options` is a Vec<CommandDataOption>
262    /// let data = CommandInputData { options, resolved: None };
263    /// let message = data.parse_field::<String>("message").unwrap();
264    ///
265    /// assert_eq!(message, Some("Hello world".to_string()));
266    /// ```
267    pub fn parse_field<T>(&self, name: &str) -> Result<Option<T>, ParseError>
268    where
269        T: CommandOption,
270    {
271        // Find command option value
272        let value = match self
273            .options
274            .iter()
275            .find(|option| option.name == name)
276            .map(|option| &option.value)
277        {
278            Some(value) => value.clone(),
279            None => return Ok(None),
280        };
281
282        // Parse command value
283        match CommandOption::from_option(
284            value,
285            CommandOptionData::default(),
286            self.resolved.as_deref(),
287        ) {
288            Ok(value) => Ok(Some(value)),
289            Err(kind) => Err(ParseError::Option(ParseOptionError {
290                field: name.to_string(),
291                kind,
292            })),
293        }
294    }
295
296    /// Get the name of the focused field.
297    ///
298    /// This method is useful when parsing commands with multiple
299    /// autocomplete fields.
300    ///
301    /// ### Example
302    /// ```
303    /// use twilight_interactions::command::CommandInputData;
304    /// # use twilight_model::application::{
305    /// #   interaction::application_command::{CommandDataOption, CommandOptionValue},
306    /// #   command::CommandOptionType,
307    /// # };
308    /// #
309    /// # let options = vec![CommandDataOption { name: "message".into(), value: CommandOptionValue::Focused("Hello world".into(), CommandOptionType::String) }];
310    ///
311    /// // `options` is a Vec<CommandDataOption>
312    /// let data = CommandInputData { options, resolved: None };
313    ///
314    /// assert_eq!(data.focused(), Some("message"));
315    /// ```
316    pub fn focused(&self) -> Option<&str> {
317        self.options
318            .iter()
319            .find(|option| matches!(option.value, CommandOptionValue::Focused(_, _)))
320            .map(|option| &*option.name)
321    }
322
323    /// Parse a subcommand's [`CommandOptionValue`].
324    ///
325    /// This method's signature is the same as the [`CommandOption`] trait,
326    /// except for the explicit `'a` lifetime. It is used when parsing
327    /// subcommands.
328    pub fn from_option(
329        value: CommandOptionValue,
330        resolved: Option<&'a InteractionDataResolved>,
331    ) -> Result<Self, ParseOptionErrorType> {
332        let options = match value {
333            CommandOptionValue::SubCommand(options)
334            | CommandOptionValue::SubCommandGroup(options) => options,
335            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
336        };
337
338        Ok(CommandInputData {
339            options,
340            resolved: resolved.map(Cow::Borrowed),
341        })
342    }
343}
344
345impl From<CommandData> for CommandInputData<'_> {
346    fn from(data: CommandData) -> Self {
347        Self {
348            options: data.options,
349            resolved: data.resolved.map(Cow::Owned),
350        }
351    }
352}
353
354/// A resolved Discord user.
355///
356/// This struct implements [`CommandOption`] and can be used to
357/// obtain resolved data for a given user ID. The struct holds
358/// a [`User`] and maybe an [`InteractionMember`].
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct ResolvedUser {
361    /// The resolved user.
362    pub resolved: User,
363    /// The resolved member, if found.
364    pub member: Option<InteractionMember>,
365}
366
367/// A resolved mentionable.
368///
369/// This struct implements [`CommandOption`] and can be used to obtain the
370/// resolved data from a mentionable ID, that can be either a user or a role.
371#[derive(Debug, Clone, PartialEq, Eq)]
372#[allow(
373    clippy::large_enum_variant,
374    reason = "minor impact, boxing would add additional indirection"
375)]
376pub enum ResolvedMentionable {
377    /// User mention.
378    User(ResolvedUser),
379    /// Role mention.
380    Role(Role),
381}
382
383impl ResolvedMentionable {
384    /// Get the ID of the mentionable.
385    pub fn id(&self) -> Id<GenericMarker> {
386        match self {
387            ResolvedMentionable::User(user) => user.resolved.id.cast(),
388            ResolvedMentionable::Role(role) => role.id.cast(),
389        }
390    }
391}
392
393/// An autocomplete command field.
394///
395/// This type represent a value parsed from an autocomplete field. See "Autocomplete interactions"
396/// in [`CommandModel` documentation] for more information.
397///
398/// [`CommandModel` documentation]: CommandModel
399#[derive(Debug, Clone, PartialEq, Eq)]
400pub enum AutocompleteValue<T> {
401    /// The field has not been completed yet.
402    None,
403    /// The field is focused by the user and being completed.
404    Focused(String),
405    /// The field has been completed by the user.
406    Completed(T),
407}
408
409macro_rules! lookup {
410    ($resolved:ident.$cat:ident, $id:expr) => {
411        $resolved
412            .and_then(|resolved| resolved.$cat.get(&$id).cloned())
413            .ok_or_else(|| ParseOptionErrorType::LookupFailed($id.get()))
414    };
415}
416
417impl CommandOption for CommandOptionValue {
418    fn from_option(
419        value: CommandOptionValue,
420        _data: CommandOptionData,
421        _resolved: Option<&InteractionDataResolved>,
422    ) -> Result<Self, ParseOptionErrorType> {
423        Ok(value)
424    }
425}
426
427impl<T> CommandOption for AutocompleteValue<T>
428where
429    T: CommandOption,
430{
431    fn from_option(
432        value: CommandOptionValue,
433        data: CommandOptionData,
434        resolved: Option<&InteractionDataResolved>,
435    ) -> Result<Self, ParseOptionErrorType> {
436        match value {
437            CommandOptionValue::Focused(value, _) => Ok(Self::Focused(value)),
438            other => {
439                let parsed = T::from_option(other, data, resolved)?;
440
441                Ok(Self::Completed(parsed))
442            }
443        }
444    }
445}
446
447impl CommandOption for String {
448    fn from_option(
449        value: CommandOptionValue,
450        data: CommandOptionData,
451        _resolved: Option<&InteractionDataResolved>,
452    ) -> Result<Self, ParseOptionErrorType> {
453        let value = match value {
454            CommandOptionValue::String(value) => value,
455            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
456        };
457
458        let char_len = value.chars().count();
459
460        if let Some(min) = data.min_length {
461            if char_len < usize::from(min) {
462                return Err(ParseOptionErrorType::StringLengthOutOfRange(value));
463            }
464        }
465
466        if let Some(max) = data.max_length {
467            if char_len > usize::from(max) {
468                return Err(ParseOptionErrorType::StringLengthOutOfRange(value));
469            }
470        }
471
472        Ok(value)
473    }
474}
475
476impl CommandOption for Cow<'_, str> {
477    fn from_option(
478        value: CommandOptionValue,
479        data: CommandOptionData,
480        resolved: Option<&InteractionDataResolved>,
481    ) -> Result<Self, ParseOptionErrorType> {
482        String::from_option(value, data, resolved).map(Cow::Owned)
483    }
484}
485
486impl CommandOption for i64 {
487    fn from_option(
488        value: CommandOptionValue,
489        data: CommandOptionData,
490        _resolved: Option<&InteractionDataResolved>,
491    ) -> Result<Self, ParseOptionErrorType> {
492        let value = match value {
493            CommandOptionValue::Integer(value) => value,
494            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
495        };
496
497        if let Some(NumberCommandOptionValue::Integer(min)) = data.min_value {
498            if value < min {
499                return Err(ParseOptionErrorType::IntegerOutOfRange(value));
500            }
501        }
502
503        if let Some(NumberCommandOptionValue::Integer(max)) = data.max_value {
504            if value > max {
505                return Err(ParseOptionErrorType::IntegerOutOfRange(value));
506            }
507        }
508
509        Ok(value)
510    }
511}
512
513impl CommandOption for f64 {
514    fn from_option(
515        value: CommandOptionValue,
516        data: CommandOptionData,
517        _resolved: Option<&InteractionDataResolved>,
518    ) -> Result<Self, ParseOptionErrorType> {
519        let value = match value {
520            CommandOptionValue::Number(value) => value,
521            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
522        };
523
524        if let Some(NumberCommandOptionValue::Number(min)) = data.min_value {
525            if value < min {
526                return Err(ParseOptionErrorType::NumberOutOfRange(value));
527            }
528        }
529
530        if let Some(NumberCommandOptionValue::Number(max)) = data.max_value {
531            if value > max {
532                return Err(ParseOptionErrorType::NumberOutOfRange(value));
533            }
534        }
535
536        Ok(value)
537    }
538}
539
540impl CommandOption for bool {
541    fn from_option(
542        value: CommandOptionValue,
543        _data: CommandOptionData,
544        _resolved: Option<&InteractionDataResolved>,
545    ) -> Result<Self, ParseOptionErrorType> {
546        match value {
547            CommandOptionValue::Boolean(value) => Ok(value),
548            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
549        }
550    }
551}
552
553impl CommandOption for Id<UserMarker> {
554    fn from_option(
555        value: CommandOptionValue,
556        _data: CommandOptionData,
557        _resolved: Option<&InteractionDataResolved>,
558    ) -> Result<Self, ParseOptionErrorType> {
559        match value {
560            CommandOptionValue::User(value) => Ok(value),
561            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
562        }
563    }
564}
565
566impl CommandOption for Id<ChannelMarker> {
567    fn from_option(
568        value: CommandOptionValue,
569        _data: CommandOptionData,
570        _resolved: Option<&InteractionDataResolved>,
571    ) -> Result<Self, ParseOptionErrorType> {
572        match value {
573            CommandOptionValue::Channel(value) => Ok(value),
574            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
575        }
576    }
577}
578
579impl CommandOption for Id<RoleMarker> {
580    fn from_option(
581        value: CommandOptionValue,
582        _data: CommandOptionData,
583        _resolved: Option<&InteractionDataResolved>,
584    ) -> Result<Self, ParseOptionErrorType> {
585        match value {
586            CommandOptionValue::Role(value) => Ok(value),
587            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
588        }
589    }
590}
591
592impl CommandOption for Id<GenericMarker> {
593    fn from_option(
594        value: CommandOptionValue,
595        _data: CommandOptionData,
596        _resolved: Option<&InteractionDataResolved>,
597    ) -> Result<Self, ParseOptionErrorType> {
598        match value {
599            CommandOptionValue::Mentionable(value) => Ok(value),
600            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
601        }
602    }
603}
604
605impl CommandOption for Id<AttachmentMarker> {
606    fn from_option(
607        value: CommandOptionValue,
608        _data: CommandOptionData,
609        _resolved: Option<&InteractionDataResolved>,
610    ) -> Result<Self, ParseOptionErrorType> {
611        match value {
612            CommandOptionValue::Attachment(value) => Ok(value),
613            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
614        }
615    }
616}
617
618impl CommandOption for Attachment {
619    fn from_option(
620        value: CommandOptionValue,
621        _data: CommandOptionData,
622        resolved: Option<&InteractionDataResolved>,
623    ) -> Result<Self, ParseOptionErrorType> {
624        let attachment_id = match value {
625            CommandOptionValue::Attachment(value) => value,
626            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
627        };
628
629        lookup!(resolved.attachments, attachment_id)
630    }
631}
632
633impl CommandOption for User {
634    fn from_option(
635        value: CommandOptionValue,
636        _data: CommandOptionData,
637        resolved: Option<&InteractionDataResolved>,
638    ) -> Result<Self, ParseOptionErrorType> {
639        let user_id = match value {
640            CommandOptionValue::User(value) => value,
641            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
642        };
643
644        lookup!(resolved.users, user_id)
645    }
646}
647
648impl CommandOption for ResolvedUser {
649    fn from_option(
650        value: CommandOptionValue,
651        _data: CommandOptionData,
652        resolved: Option<&InteractionDataResolved>,
653    ) -> Result<Self, ParseOptionErrorType> {
654        let user_id = match value {
655            CommandOptionValue::User(value) => value,
656            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
657        };
658
659        Ok(Self {
660            resolved: lookup!(resolved.users, user_id)?,
661            member: lookup!(resolved.members, user_id).ok(),
662        })
663    }
664}
665
666impl CommandOption for ResolvedMentionable {
667    fn from_option(
668        value: CommandOptionValue,
669        _data: CommandOptionData,
670        resolved: Option<&InteractionDataResolved>,
671    ) -> Result<Self, ParseOptionErrorType> {
672        let id = match value {
673            CommandOptionValue::Mentionable(value) => value,
674            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
675        };
676
677        let user_id = id.cast();
678        if let Ok(user) = lookup!(resolved.users, user_id) {
679            let resolved_user = ResolvedUser {
680                resolved: user,
681                member: lookup!(resolved.members, user_id).ok(),
682            };
683
684            return Ok(Self::User(resolved_user));
685        }
686
687        let role_id = id.cast();
688        if let Ok(role) = lookup!(resolved.roles, role_id) {
689            return Ok(Self::Role(role));
690        }
691
692        Err(ParseOptionErrorType::LookupFailed(id.into()))
693    }
694}
695
696impl CommandOption for InteractionChannel {
697    fn from_option(
698        value: CommandOptionValue,
699        data: CommandOptionData,
700        resolved: Option<&InteractionDataResolved>,
701    ) -> Result<Self, ParseOptionErrorType> {
702        let resolved = match value {
703            CommandOptionValue::Channel(value) => lookup!(resolved.channels, value)?,
704            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
705        };
706
707        if let Some(channel_types) = data.channel_types {
708            if !channel_types.contains(&resolved.kind) {
709                return Err(ParseOptionErrorType::InvalidChannelType(resolved.kind));
710            }
711        }
712
713        Ok(resolved)
714    }
715}
716
717impl CommandOption for Role {
718    fn from_option(
719        value: CommandOptionValue,
720        _data: CommandOptionData,
721        resolved: Option<&InteractionDataResolved>,
722    ) -> Result<Self, ParseOptionErrorType> {
723        let role_id = match value {
724            CommandOptionValue::Role(value) => value,
725            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
726        };
727
728        lookup!(resolved.roles, role_id)
729    }
730}