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}