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}