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        if let Some(min) = data.min_length {
459            if value.len() < usize::from(min) {
460                todo!()
461            }
462        }
463
464        if let Some(max) = data.max_length {
465            if value.len() > usize::from(max) {
466                todo!()
467            }
468        }
469
470        Ok(value)
471    }
472}
473
474impl CommandOption for Cow<'_, str> {
475    fn from_option(
476        value: CommandOptionValue,
477        data: CommandOptionData,
478        resolved: Option<&InteractionDataResolved>,
479    ) -> Result<Self, ParseOptionErrorType> {
480        String::from_option(value, data, resolved).map(Cow::Owned)
481    }
482}
483
484impl CommandOption for i64 {
485    fn from_option(
486        value: CommandOptionValue,
487        data: CommandOptionData,
488        _resolved: Option<&InteractionDataResolved>,
489    ) -> Result<Self, ParseOptionErrorType> {
490        let value = match value {
491            CommandOptionValue::Integer(value) => value,
492            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
493        };
494
495        if let Some(NumberCommandOptionValue::Integer(min)) = data.min_value {
496            if value < min {
497                return Err(ParseOptionErrorType::IntegerOutOfRange(value));
498            }
499        }
500
501        if let Some(NumberCommandOptionValue::Integer(max)) = data.max_value {
502            if value > max {
503                return Err(ParseOptionErrorType::IntegerOutOfRange(value));
504            }
505        }
506
507        Ok(value)
508    }
509}
510
511impl CommandOption for f64 {
512    fn from_option(
513        value: CommandOptionValue,
514        data: CommandOptionData,
515        _resolved: Option<&InteractionDataResolved>,
516    ) -> Result<Self, ParseOptionErrorType> {
517        let value = match value {
518            CommandOptionValue::Number(value) => value,
519            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
520        };
521
522        if let Some(NumberCommandOptionValue::Number(min)) = data.min_value {
523            if value < min {
524                return Err(ParseOptionErrorType::NumberOutOfRange(value));
525            }
526        }
527
528        if let Some(NumberCommandOptionValue::Number(max)) = data.max_value {
529            if value > max {
530                return Err(ParseOptionErrorType::NumberOutOfRange(value));
531            }
532        }
533
534        Ok(value)
535    }
536}
537
538impl CommandOption for bool {
539    fn from_option(
540        value: CommandOptionValue,
541        _data: CommandOptionData,
542        _resolved: Option<&InteractionDataResolved>,
543    ) -> Result<Self, ParseOptionErrorType> {
544        match value {
545            CommandOptionValue::Boolean(value) => Ok(value),
546            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
547        }
548    }
549}
550
551impl CommandOption for Id<UserMarker> {
552    fn from_option(
553        value: CommandOptionValue,
554        _data: CommandOptionData,
555        _resolved: Option<&InteractionDataResolved>,
556    ) -> Result<Self, ParseOptionErrorType> {
557        match value {
558            CommandOptionValue::User(value) => Ok(value),
559            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
560        }
561    }
562}
563
564impl CommandOption for Id<ChannelMarker> {
565    fn from_option(
566        value: CommandOptionValue,
567        _data: CommandOptionData,
568        _resolved: Option<&InteractionDataResolved>,
569    ) -> Result<Self, ParseOptionErrorType> {
570        match value {
571            CommandOptionValue::Channel(value) => Ok(value),
572            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
573        }
574    }
575}
576
577impl CommandOption for Id<RoleMarker> {
578    fn from_option(
579        value: CommandOptionValue,
580        _data: CommandOptionData,
581        _resolved: Option<&InteractionDataResolved>,
582    ) -> Result<Self, ParseOptionErrorType> {
583        match value {
584            CommandOptionValue::Role(value) => Ok(value),
585            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
586        }
587    }
588}
589
590impl CommandOption for Id<GenericMarker> {
591    fn from_option(
592        value: CommandOptionValue,
593        _data: CommandOptionData,
594        _resolved: Option<&InteractionDataResolved>,
595    ) -> Result<Self, ParseOptionErrorType> {
596        match value {
597            CommandOptionValue::Mentionable(value) => Ok(value),
598            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
599        }
600    }
601}
602
603impl CommandOption for Id<AttachmentMarker> {
604    fn from_option(
605        value: CommandOptionValue,
606        _data: CommandOptionData,
607        _resolved: Option<&InteractionDataResolved>,
608    ) -> Result<Self, ParseOptionErrorType> {
609        match value {
610            CommandOptionValue::Attachment(value) => Ok(value),
611            other => Err(ParseOptionErrorType::InvalidType(other.kind())),
612        }
613    }
614}
615
616impl CommandOption for Attachment {
617    fn from_option(
618        value: CommandOptionValue,
619        _data: CommandOptionData,
620        resolved: Option<&InteractionDataResolved>,
621    ) -> Result<Self, ParseOptionErrorType> {
622        let attachment_id = match value {
623            CommandOptionValue::Attachment(value) => value,
624            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
625        };
626
627        lookup!(resolved.attachments, attachment_id)
628    }
629}
630
631impl CommandOption for User {
632    fn from_option(
633        value: CommandOptionValue,
634        _data: CommandOptionData,
635        resolved: Option<&InteractionDataResolved>,
636    ) -> Result<Self, ParseOptionErrorType> {
637        let user_id = match value {
638            CommandOptionValue::User(value) => value,
639            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
640        };
641
642        lookup!(resolved.users, user_id)
643    }
644}
645
646impl CommandOption for ResolvedUser {
647    fn from_option(
648        value: CommandOptionValue,
649        _data: CommandOptionData,
650        resolved: Option<&InteractionDataResolved>,
651    ) -> Result<Self, ParseOptionErrorType> {
652        let user_id = match value {
653            CommandOptionValue::User(value) => value,
654            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
655        };
656
657        Ok(Self {
658            resolved: lookup!(resolved.users, user_id)?,
659            member: lookup!(resolved.members, user_id).ok(),
660        })
661    }
662}
663
664impl CommandOption for ResolvedMentionable {
665    fn from_option(
666        value: CommandOptionValue,
667        _data: CommandOptionData,
668        resolved: Option<&InteractionDataResolved>,
669    ) -> Result<Self, ParseOptionErrorType> {
670        let id = match value {
671            CommandOptionValue::Mentionable(value) => value,
672            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
673        };
674
675        let user_id = id.cast();
676        if let Ok(user) = lookup!(resolved.users, user_id) {
677            let resolved_user = ResolvedUser {
678                resolved: user,
679                member: lookup!(resolved.members, user_id).ok(),
680            };
681
682            return Ok(Self::User(resolved_user));
683        }
684
685        let role_id = id.cast();
686        if let Ok(role) = lookup!(resolved.roles, role_id) {
687            return Ok(Self::Role(role));
688        }
689
690        Err(ParseOptionErrorType::LookupFailed(id.into()))
691    }
692}
693
694impl CommandOption for InteractionChannel {
695    fn from_option(
696        value: CommandOptionValue,
697        data: CommandOptionData,
698        resolved: Option<&InteractionDataResolved>,
699    ) -> Result<Self, ParseOptionErrorType> {
700        let resolved = match value {
701            CommandOptionValue::Channel(value) => lookup!(resolved.channels, value)?,
702            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
703        };
704
705        if let Some(channel_types) = data.channel_types {
706            if !channel_types.contains(&resolved.kind) {
707                return Err(ParseOptionErrorType::InvalidChannelType(resolved.kind));
708            }
709        }
710
711        Ok(resolved)
712    }
713}
714
715impl CommandOption for Role {
716    fn from_option(
717        value: CommandOptionValue,
718        _data: CommandOptionData,
719        resolved: Option<&InteractionDataResolved>,
720    ) -> Result<Self, ParseOptionErrorType> {
721        let role_id = match value {
722            CommandOptionValue::Role(value) => value,
723            other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
724        };
725
726        lookup!(resolved.roles, role_id)
727    }
728}