serenity/builder/create_command.rs
1#[cfg(feature = "http")]
2use super::Builder;
3#[cfg(feature = "http")]
4use crate::http::CacheHttp;
5use crate::internal::prelude::*;
6use crate::model::prelude::*;
7
8/// A builder for creating a new [`CommandOption`].
9///
10/// [`Self::kind`], [`Self::name`], and [`Self::description`] are required fields.
11///
12/// [`CommandOption`]: crate::model::application::CommandOption
13///
14/// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure).
15#[derive(Clone, Debug, Serialize)]
16#[must_use]
17pub struct CreateCommandOption(CommandOption);
18
19impl CreateCommandOption {
20 /// Creates a new builder with the given option type, name, and description, leaving all other
21 /// fields empty.
22 pub fn new(
23 kind: CommandOptionType,
24 name: impl Into<String>,
25 description: impl Into<String>,
26 ) -> Self {
27 Self(CommandOption {
28 kind,
29 name: name.into(),
30 name_localizations: None,
31 description: description.into(),
32 description_localizations: None,
33 required: false,
34 autocomplete: false,
35 min_value: None,
36 max_value: None,
37 min_length: None,
38 max_length: None,
39
40 channel_types: Vec::new(),
41 choices: Vec::new(),
42 options: Vec::new(),
43 })
44 }
45
46 /// Sets the `CommandOptionType`, replacing the current value as set in [`Self::new`].
47 pub fn kind(mut self, kind: CommandOptionType) -> Self {
48 self.0.kind = kind;
49 self
50 }
51
52 /// Sets the name of the option, replacing the current value as set in [`Self::new`].
53 ///
54 /// **Note**: Must be between 1 and 32 lowercase characters, matching `r"^[\w-]{1,32}$"`.
55 pub fn name(mut self, name: impl Into<String>) -> Self {
56 self.0.name = name.into();
57 self
58 }
59
60 /// Specifies a localized name of the option.
61 ///
62 /// ```rust
63 /// # use serenity::builder::CreateCommandOption;
64 /// # use serenity::model::application::CommandOptionType;
65 /// # CreateCommandOption::new(CommandOptionType::Integer, "", "")
66 /// .name("age")
67 /// .name_localized("zh-CN", "岁数")
68 /// # ;
69 /// ```
70 pub fn name_localized(mut self, locale: impl Into<String>, name: impl Into<String>) -> Self {
71 let map = self.0.name_localizations.get_or_insert_with(Default::default);
72 map.insert(locale.into(), name.into());
73 self
74 }
75
76 /// Sets the description for the option, replacing the current value as set in [`Self::new`].
77 ///
78 /// **Note**: Must be between 1 and 100 characters.
79 pub fn description(mut self, description: impl Into<String>) -> Self {
80 self.0.description = description.into();
81 self
82 }
83 /// Specifies a localized description of the option.
84 ///
85 /// ```rust
86 /// # use serenity::builder::CreateCommandOption;
87 /// # use serenity::model::application::CommandOptionType;
88 /// # CreateCommandOption::new(CommandOptionType::String, "", "")
89 /// .description("Wish a friend a happy birthday")
90 /// .description_localized("zh-CN", "祝你朋友生日快乐")
91 /// # ;
92 /// ```
93 pub fn description_localized(
94 mut self,
95 locale: impl Into<String>,
96 description: impl Into<String>,
97 ) -> Self {
98 let map = self.0.description_localizations.get_or_insert_with(Default::default);
99 map.insert(locale.into(), description.into());
100 self
101 }
102
103 /// Sets if this option is required or optional.
104 ///
105 /// **Note**: This defaults to `false`.
106 pub fn required(mut self, required: bool) -> Self {
107 self.0.required = required;
108 self
109 }
110
111 /// Adds an optional int-choice.
112 ///
113 /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100
114 /// characters. Value must be between -2^53 and 2^53.
115 pub fn add_int_choice(self, name: impl Into<String>, value: i32) -> Self {
116 self.add_choice(CommandOptionChoice {
117 name: name.into(),
118 value: Value::from(value),
119 name_localizations: None,
120 })
121 }
122
123 /// Adds a localized optional int-choice. See [`Self::add_int_choice`] for more info.
124 pub fn add_int_choice_localized(
125 self,
126 name: impl Into<String>,
127 value: i32,
128 locales: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
129 ) -> Self {
130 self.add_choice(CommandOptionChoice {
131 name: name.into(),
132 value: Value::from(value),
133 name_localizations: Some(
134 locales.into_iter().map(|(l, n)| (l.into(), n.into())).collect(),
135 ),
136 })
137 }
138
139 /// Adds an optional string-choice.
140 ///
141 /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100
142 /// characters. Value must be up to 100 characters.
143 pub fn add_string_choice(self, name: impl Into<String>, value: impl Into<String>) -> Self {
144 self.add_choice(CommandOptionChoice {
145 name: name.into(),
146 value: Value::String(value.into()),
147 name_localizations: None,
148 })
149 }
150
151 /// Adds a localized optional string-choice. See [`Self::add_string_choice`] for more info.
152 pub fn add_string_choice_localized(
153 self,
154 name: impl Into<String>,
155 value: impl Into<String>,
156 locales: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
157 ) -> Self {
158 self.add_choice(CommandOptionChoice {
159 name: name.into(),
160 value: Value::String(value.into()),
161 name_localizations: Some(
162 locales.into_iter().map(|(l, n)| (l.into(), n.into())).collect(),
163 ),
164 })
165 }
166
167 /// Adds an optional number-choice.
168 ///
169 /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100
170 /// characters. Value must be between -2^53 and 2^53.
171 pub fn add_number_choice(self, name: impl Into<String>, value: f64) -> Self {
172 self.add_choice(CommandOptionChoice {
173 name: name.into(),
174 value: Value::from(value),
175 name_localizations: None,
176 })
177 }
178
179 /// Adds a localized optional number-choice. See [`Self::add_number_choice`] for more info.
180 pub fn add_number_choice_localized(
181 self,
182 name: impl Into<String>,
183 value: f64,
184 locales: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
185 ) -> Self {
186 self.add_choice(CommandOptionChoice {
187 name: name.into(),
188 value: Value::from(value),
189 name_localizations: Some(
190 locales.into_iter().map(|(l, n)| (l.into(), n.into())).collect(),
191 ),
192 })
193 }
194
195 fn add_choice(mut self, value: CommandOptionChoice) -> Self {
196 self.0.choices.push(value);
197 self
198 }
199
200 /// Optionally enable/disable autocomplete interactions for this option.
201 ///
202 /// **Notes**:
203 /// - May not be set to `true` if `choices` are set
204 /// - Options using `autocomplete` are not confined to only use given choices
205 pub fn set_autocomplete(mut self, value: bool) -> Self {
206 self.0.autocomplete = value;
207 self
208 }
209
210 /// If the option is a [`SubCommandGroup`] or [`SubCommand`], nested options are its parameters.
211 ///
212 /// This will overwrite any existing sub-options. To add a sub-option to the existing list, use
213 /// [`Self::add_sub_option`].
214 ///
215 /// **Note**: A command can have up to 25 subcommand groups, or subcommands. A subcommand group
216 /// can have up to 25 subcommands. A subcommand can have up to 25 options.
217 ///
218 /// [`SubCommandGroup`]: crate::model::application::CommandOptionType::SubCommandGroup
219 /// [`SubCommand`]: crate::model::application::CommandOptionType::SubCommand
220 pub fn set_sub_options(
221 mut self,
222 sub_options: impl IntoIterator<Item = CreateCommandOption>,
223 ) -> Self {
224 self.0.options = sub_options.into_iter().map(|o| o.0).collect();
225 self
226 }
227
228 /// If the option is a [`SubCommandGroup`] or [`SubCommand`], nested options are its parameters.
229 ///
230 /// **Note**: A command can have up to 25 subcommand groups, or subcommands. A subcommand group
231 /// can have up to 25 subcommands. A subcommand can have up to 25 options.
232 ///
233 /// [`SubCommandGroup`]: crate::model::application::CommandOptionType::SubCommandGroup
234 /// [`SubCommand`]: crate::model::application::CommandOptionType::SubCommand
235 pub fn add_sub_option(mut self, sub_option: CreateCommandOption) -> Self {
236 self.0.options.push(sub_option.0);
237 self
238 }
239
240 /// If the option is a [`Channel`], it will only be able to show these types.
241 ///
242 /// [`Channel`]: crate::model::application::CommandOptionType::Channel
243 pub fn channel_types(mut self, channel_types: Vec<ChannelType>) -> Self {
244 self.0.channel_types = channel_types;
245 self
246 }
247
248 /// Sets the minimum permitted value for this integer option
249 pub fn min_int_value(mut self, value: u64) -> Self {
250 self.0.min_value = Some(value.into());
251 self
252 }
253
254 /// Sets the maximum permitted value for this integer option
255 pub fn max_int_value(mut self, value: u64) -> Self {
256 self.0.max_value = Some(value.into());
257 self
258 }
259
260 /// Sets the minimum permitted value for this number option
261 pub fn min_number_value(mut self, value: f64) -> Self {
262 self.0.min_value = serde_json::Number::from_f64(value);
263 self
264 }
265
266 /// Sets the maximum permitted value for this number option
267 pub fn max_number_value(mut self, value: f64) -> Self {
268 self.0.max_value = serde_json::Number::from_f64(value);
269 self
270 }
271
272 /// Sets the minimum permitted length for this string option.
273 ///
274 /// The value of `min_length` must be greater or equal to `0`.
275 pub fn min_length(mut self, value: u16) -> Self {
276 self.0.min_length = Some(value);
277
278 self
279 }
280
281 /// Sets the maximum permitted length for this string option.
282 ///
283 /// The value of `max_length` must be greater or equal to `1`.
284 pub fn max_length(mut self, value: u16) -> Self {
285 self.0.max_length = Some(value);
286
287 self
288 }
289}
290
291/// A builder for creating a new [`Command`].
292///
293/// [`Self::name`] and [`Self::description`] are required fields.
294///
295/// [`Command`]: crate::model::application::Command
296///
297/// Discord docs:
298/// - [global command](https://discord.com/developers/docs/interactions/application-commands#create-global-application-command-json-params)
299/// - [guild command](https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command-json-params)
300#[derive(Clone, Debug, Serialize)]
301#[must_use]
302pub struct CreateCommand {
303 name: String,
304 name_localizations: HashMap<String, String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 description: Option<String>,
307 description_localizations: HashMap<String, String>,
308 options: Vec<CreateCommandOption>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 default_member_permissions: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 dm_permission: Option<bool>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 #[serde(rename = "type")]
315 kind: Option<CommandType>,
316 #[serde(skip_serializing_if = "Option::is_none")]
317 integration_types: Option<Vec<InstallationContext>>,
318 #[serde(skip_serializing_if = "Option::is_none")]
319 contexts: Option<Vec<InteractionContext>>,
320 nsfw: bool,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 handler: Option<EntryPointHandlerType>,
323}
324
325impl CreateCommand {
326 /// Creates a new builder with the given name, leaving all other fields empty.
327 pub fn new(name: impl Into<String>) -> Self {
328 Self {
329 kind: None,
330
331 name: name.into(),
332 name_localizations: HashMap::new(),
333 description: None,
334 description_localizations: HashMap::new(),
335 default_member_permissions: None,
336 dm_permission: None,
337
338 integration_types: None,
339 contexts: None,
340
341 options: Vec::new(),
342 nsfw: false,
343 handler: None,
344 }
345 }
346
347 /// Specifies the name of the application command, replacing the current value as set in
348 /// [`Self::new`].
349 ///
350 /// **Note**: Must be between 1 and 32 lowercase characters, matching `r"^[\w-]{1,32}$"`. Two
351 /// global commands of the same app cannot have the same name. Two guild-specific commands of
352 /// the same app cannot have the same name.
353 pub fn name(mut self, name: impl Into<String>) -> Self {
354 self.name = name.into();
355 self
356 }
357
358 /// Specifies a localized name of the application command.
359 ///
360 /// ```rust
361 /// # serenity::builder::CreateCommand::new("")
362 /// .name("birthday")
363 /// .name_localized("zh-CN", "生日")
364 /// .name_localized("el", "γενέθλια")
365 /// # ;
366 /// ```
367 pub fn name_localized(mut self, locale: impl Into<String>, name: impl Into<String>) -> Self {
368 self.name_localizations.insert(locale.into(), name.into());
369 self
370 }
371
372 /// Specifies the type of the application command.
373 pub fn kind(mut self, kind: CommandType) -> Self {
374 self.kind = Some(kind);
375 self
376 }
377
378 /// Specifies the default permissions required to execute the command.
379 pub fn default_member_permissions(mut self, permissions: Permissions) -> Self {
380 self.default_member_permissions = Some(permissions.bits().to_string());
381 self
382 }
383
384 /// Specifies if the command is available in DMs.
385 #[cfg_attr(feature = "unstable_discord_api", deprecated = "Use contexts instead")]
386 pub fn dm_permission(mut self, enabled: bool) -> Self {
387 self.dm_permission = Some(enabled);
388 self
389 }
390
391 /// Specifies the description of the application command.
392 ///
393 /// **Note**: Must be between 1 and 100 characters long.
394 pub fn description(mut self, description: impl Into<String>) -> Self {
395 self.description = Some(description.into());
396 self
397 }
398
399 /// Specifies a localized description of the application command.
400 ///
401 /// ```rust
402 /// # serenity::builder::CreateCommand::new("")
403 /// .description("Wish a friend a happy birthday")
404 /// .description_localized("zh-CN", "祝你朋友生日快乐")
405 /// # ;
406 /// ```
407 pub fn description_localized(
408 mut self,
409 locale: impl Into<String>,
410 description: impl Into<String>,
411 ) -> Self {
412 self.description_localizations.insert(locale.into(), description.into());
413 self
414 }
415
416 /// Adds an application command option for the application command.
417 ///
418 /// **Note**: Application commands can have up to 25 options.
419 pub fn add_option(mut self, option: CreateCommandOption) -> Self {
420 self.options.push(option);
421 self
422 }
423
424 /// Sets all the application command options for the application command.
425 ///
426 /// **Note**: Application commands can have up to 25 options.
427 pub fn set_options(mut self, options: Vec<CreateCommandOption>) -> Self {
428 self.options = options;
429 self
430 }
431
432 /// Adds an installation context that this application command can be used in.
433 pub fn add_integration_type(mut self, integration_type: InstallationContext) -> Self {
434 self.integration_types.get_or_insert_with(Vec::default).push(integration_type);
435 self
436 }
437
438 /// Sets the installation contexts that this application command can be used in.
439 pub fn integration_types(mut self, integration_types: Vec<InstallationContext>) -> Self {
440 self.integration_types = Some(integration_types);
441 self
442 }
443
444 /// Adds an interaction context that this application command can be used in.
445 pub fn add_context(mut self, context: InteractionContext) -> Self {
446 self.contexts.get_or_insert_with(Vec::default).push(context);
447 self
448 }
449
450 /// Sets the interaction contexts that this application command can be used in.
451 pub fn contexts(mut self, contexts: Vec<InteractionContext>) -> Self {
452 self.contexts = Some(contexts);
453 self
454 }
455
456 /// Whether this command is marked NSFW (age-restricted)
457 pub fn nsfw(mut self, nsfw: bool) -> Self {
458 self.nsfw = nsfw;
459 self
460 }
461
462 /// Sets the command's entry point handler type. Only valid for commands of type
463 /// [`PrimaryEntryPoint`].
464 ///
465 /// [`PrimaryEntryPoint`]: CommandType::PrimaryEntryPoint
466 pub fn handler(mut self, handler: EntryPointHandlerType) -> Self {
467 self.handler = Some(handler);
468 self
469 }
470}
471
472#[cfg(feature = "http")]
473#[async_trait::async_trait]
474impl Builder for CreateCommand {
475 type Context<'ctx> = (Option<GuildId>, Option<CommandId>);
476 type Built = Command;
477
478 /// Create a [`Command`], overriding an existing one with the same name if it exists.
479 ///
480 /// Providing a [`GuildId`] will create a command in the corresponding [`Guild`]. Otherwise, a
481 /// global command will be created.
482 ///
483 /// Providing a [`CommandId`] will edit the corresponding command.
484 ///
485 /// # Errors
486 ///
487 /// Returns [`Error::Http`] if invalid data is given. See [Discord's docs] for more details.
488 ///
489 /// May also return [`Error::Json`] if there is an error in deserializing the API response.
490 ///
491 /// [Discord's docs]: https://discord.com/developers/docs/interactions/slash-commands
492 async fn execute(
493 self,
494 cache_http: impl CacheHttp,
495 ctx: Self::Context<'_>,
496 ) -> Result<Self::Built> {
497 let http = cache_http.http();
498 match ctx {
499 (Some(guild_id), Some(cmd_id)) => {
500 http.edit_guild_command(guild_id, cmd_id, &self).await
501 },
502 (Some(guild_id), None) => http.create_guild_command(guild_id, &self).await,
503 (None, Some(cmd_id)) => http.edit_global_command(cmd_id, &self).await,
504 (None, None) => http.create_global_command(&self).await,
505 }
506 }
507}