twilight_interactions/command/
create_command.rs

1use std::{borrow::Cow, collections::HashMap};
2
3use twilight_model::{
4    application::{
5        command::{Command, CommandOption, CommandOptionType, CommandType},
6        interaction::{InteractionChannel, InteractionContextType},
7    },
8    channel::Attachment,
9    guild::{Permissions, Role},
10    id::{
11        marker::{AttachmentMarker, ChannelMarker, GenericMarker, RoleMarker, UserMarker},
12        Id,
13    },
14    oauth::ApplicationIntegrationType,
15    user::User,
16};
17
18use super::{internal::CreateOptionData, ResolvedMentionable, ResolvedUser};
19
20/// Create a slash command from a type.
21///
22/// This trait is used to create commands from command models. A derive
23/// macro is provided to automatically implement the traits.
24///
25/// ## Types and fields documentation
26/// The trait can be derived on structs whose fields implement [`CreateOption`]
27/// (see the [module documentation](crate::command) for a list of supported
28/// types) or enums whose variants implement [`CreateCommand`].
29///
30/// Unlike the [`CommandModel`] trait, all fields or variants of the type it's
31/// implemented on must have a description. The description corresponds either
32/// to the first line of the documentation comment or the value of the `desc`
33/// attribute. The type must also be named with the `name` attribute.
34///
35/// ## Example
36/// ```
37/// # use twilight_model::guild::Permissions;
38/// use twilight_interactions::command::{CreateCommand, ResolvedUser};
39///
40/// #[derive(CreateCommand)]
41/// #[command(
42///     name = "hello",
43///     desc = "Say hello",
44///     default_permissions = "hello_permissions"
45/// )]
46/// struct HelloCommand {
47///     /// The message to send.
48///     message: String,
49///     /// The user to send the message to.
50///     user: Option<ResolvedUser>,
51/// }
52///
53/// fn hello_permissions() -> Permissions {
54///     Permissions::SEND_MESSAGES
55/// }
56/// ```
57///
58/// ## Macro attributes
59/// The macro provides a `#[command]` attribute to provide additional
60/// information.
61///
62/// | Attribute                  | Type                | Location               | Description                                                               |
63/// |----------------------------|---------------------|------------------------|---------------------------------------------------------------------------|
64/// | `name`                     | `str`               | Type                   | Name of the command (required).                                           |
65/// | `desc`                     | `str`               | Type / Field / Variant | Description of the command (required).                                    |
66/// | `default_permissions`      | `fn`[^perms]        | Type                   | Default permissions required by members to run the command.               |
67/// | `dm_permission`            | `bool`              | Type                   | Whether the command can be run in DMs.                                    |
68/// | `nsfw`                     | `bool`              | Type                   | Whether the command is age-restricted.                                    |
69/// | `rename`                   | `str`               | Field                  | Use a different option name than the field name.                          |
70/// | `name_localizations`       | `fn`[^localization] | Type / Field / Variant | Localized name of the command (optional).                                 |
71/// | `desc_localizations`       | `fn`[^localization] | Type / Field / Variant | Localized description of the command (optional).                          |
72/// | `autocomplete`             | `bool`              | Field                  | Enable autocomplete on this field.                                        |
73/// | `channel_types`            | `str`               | Field                  | Restricts the channel choice to specific types.[^channel_types]           |
74/// | `max_value`, `min_value`   | `i64` or `f64`      | Field                  | Set the maximum and/or minimum value permitted.                           |
75/// | `max_length`, `min_length` | `u16`               | Field                  | Maximum and/or minimum string length permitted.                           |
76/// | `contexts`                 | `str`               | Type                   | Interaction context(s) where the command can be used.[^contexts]          |
77/// | `integration_types`        | `str`               | Type                   | Installation contexts where the command is available.[^integration_types] |
78///
79/// [^perms]: Path to a function that returns [`Permissions`]. Permissions can
80/// only be set on top-level commands
81///
82/// [^localization]: Path to a function that returns a type that implements
83/// `IntoIterator<Item = (ToString, ToString)>`. See the module documentation to
84/// learn more.
85///
86/// [^channel_types]: List of [`ChannelType`] names in snake_case separated by spaces
87/// like `guild_text private`.
88///
89/// [^contexts]: List of [`InteractionContextType`] names in snake_case separated by
90/// spaces like `guild private_channel`.
91///
92/// [^integration_types]: List of [`ApplicationIntegrationType`] names in snake_case
93/// separated by spaces like `guild_install user_install`.
94///
95/// [`CommandModel`]: super::CommandModel
96/// [`ChannelType`]: twilight_model::channel::ChannelType
97/// [`InteractionContextType`]: twilight_model::application::interaction::InteractionContextType
98/// [`ApplicationIntegrationType`]: twilight_model::oauth::ApplicationIntegrationType
99pub trait CreateCommand: Sized {
100    /// Name of the command.
101    const NAME: &'static str;
102
103    /// Create an [`ApplicationCommandData`] for this type.
104    fn create_command() -> ApplicationCommandData;
105}
106
107impl<T: CreateCommand> CreateCommand for Box<T> {
108    const NAME: &'static str = T::NAME;
109
110    fn create_command() -> ApplicationCommandData {
111        T::create_command()
112    }
113}
114
115/// Create a command option from a type.
116///
117/// This trait is used by the implementation of [`CreateCommand`] generated
118/// by the derive macro. See the [module documentation](crate::command) for
119/// a list of implemented types.
120///
121/// ## Option choices
122/// This trait can be derived on enums to represent command options with
123/// predefined choices. The `#[option]` attribute must be present on each
124/// variant.
125///
126/// ### Example
127/// ```
128/// use twilight_interactions::command::CreateOption;
129///
130/// #[derive(CreateOption)]
131/// enum TimeUnit {
132///     #[option(name = "Minute", value = 60)]
133///     Minute,
134///     #[option(name = "Hour", value = 3600)]
135///     Hour,
136///     #[option(name = "Day", value = 86400)]
137///     Day,
138/// }
139/// ```
140///
141/// ### Macro attributes
142/// The macro provides an `#[option]` attribute to configure the generated code.
143///
144/// | Attribute            | Type                  | Location | Description                                  |
145/// |----------------------|-----------------------|----------|----------------------------------------------|
146/// | `name`               | `str`                 | Variant  | Set the name of the command option choice.   |
147/// | `name_localizations` | `fn`[^localization]   | Variant  | Localized name of the command option choice. |
148/// | `value`              | `str`, `i64` or `f64` | Variant  | Value of the command option choice.          |
149///
150/// [^localization]: Path to a function that returns a type that implements
151///                  `IntoIterator<Item = (ToString, ToString)>`. See the
152///                  [module documentation](crate::command) to learn more.
153pub trait CreateOption: Sized {
154    /// Create a [`CommandOption`] from this type.
155    fn create_option(data: CreateOptionData) -> CommandOption;
156}
157
158/// Localization data for command names.
159///
160/// This type is used in the `name_localizations` attribute of the
161/// [`CreateCommand`] and [`CreateOption`] traits. See the [module
162/// documentation](crate::command) for more information.
163#[derive(Debug, Clone, PartialEq)]
164pub struct NameLocalizations {
165    pub(crate) localizations: HashMap<String, String>,
166}
167
168impl NameLocalizations {
169    /// Create a new [`NameLocalizations`].
170    ///
171    /// The localizations must be a tuple where the first element is a valid
172    /// [Discord locale] and the second element is the localized value.
173    ///
174    /// See [Localization] on Discord Developer Docs for more information.
175    ///
176    /// [Discord locale]: https://discord.com/developers/docs/reference#locales
177    /// [Localization]: https://discord.com/developers/docs/interactions/application-commands#localization
178    pub fn new(
179        localizations: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
180    ) -> Self {
181        let localizations = localizations
182            .into_iter()
183            .map(|(k, v)| (k.into(), v.into()))
184            .collect();
185
186        Self { localizations }
187    }
188}
189
190/// Localization data for command descriptions.
191///
192/// This type is used in the `desc_localizations` attribute of the
193/// [`CreateCommand`] trait. See the [module documentation](crate::command) for
194/// more information.
195#[derive(Debug, Clone, PartialEq)]
196pub struct DescLocalizations {
197    pub(crate) fallback: String,
198    pub(crate) localizations: HashMap<String, String>,
199}
200
201impl DescLocalizations {
202    /// Create a new [`DescLocalizations`].
203    ///
204    /// The localizations must be a tuple where the first element is a valid
205    /// [Discord locale] and the second element is the localized value.
206    ///
207    /// See [Localization] on Discord Developer Docs for more information.
208    ///
209    /// [Discord locale]: https://discord.com/developers/docs/reference#locales
210    /// [Localization]: https://discord.com/developers/docs/interactions/application-commands#localization
211    pub fn new(
212        fallback: impl Into<String>,
213        localizations: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
214    ) -> Self {
215        let fallback = fallback.into();
216        let localizations = localizations
217            .into_iter()
218            .map(|(k, v)| (k.into(), v.into()))
219            .collect();
220
221        Self {
222            fallback,
223            localizations,
224        }
225    }
226}
227
228/// Data sent to Discord to create a command.
229///
230/// This type is used in the [`CreateCommand`] trait.
231/// To convert it into a [`Command`], use the [From] (or [Into]) trait.
232#[derive(Debug, Clone, PartialEq)]
233pub struct ApplicationCommandData {
234    /// Name of the command. It must be 32 characters or less.
235    pub name: String,
236    /// Localization dictionary for the command name. Keys must be valid
237    /// locales.
238    pub name_localizations: Option<HashMap<String, String>>,
239    /// Description of the command. It must be 100 characters or less.
240    pub description: String,
241    /// Localization dictionary for the command description. Keys must be valid
242    /// locales.
243    pub description_localizations: Option<HashMap<String, String>>,
244    /// List of command options.
245    pub options: Vec<CommandOption>,
246    /// Whether the command is available in DMs.
247    #[deprecated(note = "use contexts instead")]
248    pub dm_permission: Option<bool>,
249    /// Default permissions required for a member to run the command.
250    pub default_member_permissions: Option<Permissions>,
251    /// Whether the command is a subcommand group.
252    pub group: bool,
253    /// Whether the command is nsfw.
254    pub nsfw: Option<bool>,
255    /// Interaction context(s) where the command can be used.
256    pub contexts: Option<Vec<InteractionContextType>>,
257    /// Installation contexts where the command is available.
258    pub integration_types: Option<Vec<ApplicationIntegrationType>>,
259}
260
261impl From<ApplicationCommandData> for Command {
262    fn from(item: ApplicationCommandData) -> Self {
263        #[allow(deprecated)]
264        Command {
265            application_id: None,
266            guild_id: None,
267            name: item.name,
268            name_localizations: item.name_localizations,
269            default_member_permissions: item.default_member_permissions,
270            dm_permission: item.dm_permission,
271            description: item.description,
272            description_localizations: item.description_localizations,
273            id: None,
274            kind: CommandType::ChatInput,
275            nsfw: item.nsfw,
276            options: item.options,
277            version: Id::new(1),
278            contexts: item.contexts,
279            integration_types: item.integration_types,
280        }
281    }
282}
283
284impl From<ApplicationCommandData> for CommandOption {
285    fn from(item: ApplicationCommandData) -> Self {
286        let data = CreateOptionData {
287            name: item.name,
288            name_localizations: item.name_localizations,
289            description: item.description,
290            description_localizations: item.description_localizations,
291            required: None,
292            autocomplete: false,
293            data: Default::default(),
294        };
295
296        if item.group {
297            data.builder(CommandOptionType::SubCommandGroup)
298                .options(item.options)
299                .build()
300        } else {
301            data.builder(CommandOptionType::SubCommand)
302                .options(item.options)
303                .build()
304        }
305    }
306}
307
308impl CreateOption for String {
309    fn create_option(data: CreateOptionData) -> CommandOption {
310        data.into_option(CommandOptionType::String)
311    }
312}
313
314impl CreateOption for Cow<'_, str> {
315    fn create_option(data: CreateOptionData) -> CommandOption {
316        data.into_option(CommandOptionType::String)
317    }
318}
319
320impl CreateOption for i64 {
321    fn create_option(data: CreateOptionData) -> CommandOption {
322        data.into_option(CommandOptionType::Integer)
323    }
324}
325
326impl CreateOption for f64 {
327    fn create_option(data: CreateOptionData) -> CommandOption {
328        data.into_option(CommandOptionType::Number)
329    }
330}
331
332impl CreateOption for bool {
333    fn create_option(data: CreateOptionData) -> CommandOption {
334        data.into_option(CommandOptionType::Boolean)
335    }
336}
337
338impl CreateOption for Id<UserMarker> {
339    fn create_option(data: CreateOptionData) -> CommandOption {
340        data.into_option(CommandOptionType::User)
341    }
342}
343
344impl CreateOption for Id<ChannelMarker> {
345    fn create_option(data: CreateOptionData) -> CommandOption {
346        data.into_option(CommandOptionType::Channel)
347    }
348}
349
350impl CreateOption for Id<RoleMarker> {
351    fn create_option(data: CreateOptionData) -> CommandOption {
352        data.into_option(CommandOptionType::Role)
353    }
354}
355
356impl CreateOption for Id<GenericMarker> {
357    fn create_option(data: CreateOptionData) -> CommandOption {
358        data.into_option(CommandOptionType::Mentionable)
359    }
360}
361
362impl CreateOption for Id<AttachmentMarker> {
363    fn create_option(data: CreateOptionData) -> CommandOption {
364        data.into_option(CommandOptionType::Attachment)
365    }
366}
367
368impl CreateOption for Attachment {
369    fn create_option(data: CreateOptionData) -> CommandOption {
370        data.into_option(CommandOptionType::Attachment)
371    }
372}
373
374impl CreateOption for User {
375    fn create_option(data: CreateOptionData) -> CommandOption {
376        data.into_option(CommandOptionType::User)
377    }
378}
379
380impl CreateOption for ResolvedUser {
381    fn create_option(data: CreateOptionData) -> CommandOption {
382        data.into_option(CommandOptionType::User)
383    }
384}
385
386impl CreateOption for ResolvedMentionable {
387    fn create_option(data: CreateOptionData) -> CommandOption {
388        data.into_option(CommandOptionType::Mentionable)
389    }
390}
391
392impl CreateOption for InteractionChannel {
393    fn create_option(data: CreateOptionData) -> CommandOption {
394        data.into_option(CommandOptionType::Channel)
395    }
396}
397
398impl CreateOption for Role {
399    fn create_option(data: CreateOptionData) -> CommandOption {
400        data.into_option(CommandOptionType::Role)
401    }
402}