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}