From 9eec5200ed19473b3f49818adf8504de6f715016 Mon Sep 17 00:00:00 2001 From: Michael Krasnitski <42564254+mkrasnitski@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:31:46 -0800 Subject: [PATCH] Add `EditCommand` builder (#3028) This separates the the builders for creating versus editing a command, since it's not possible to change the `type` of a command (in serenity this is the `kind` field). Also, the `name` field is not required when editing a command. --- src/builder/create_command.rs | 94 +++++----------- src/builder/edit_command.rs | 179 +++++++++++++++++++++++++++++++ src/builder/mod.rs | 2 + src/model/application/command.rs | 8 +- src/model/guild/guild_id.rs | 7 +- 5 files changed, 216 insertions(+), 74 deletions(-) create mode 100644 src/builder/edit_command.rs diff --git a/src/builder/create_command.rs b/src/builder/create_command.rs index 3ce94af55ab..909e2a975d8 100644 --- a/src/builder/create_command.rs +++ b/src/builder/create_command.rs @@ -1,9 +1,9 @@ use std::borrow::Cow; use std::collections::HashMap; +use crate::builder::EditCommand; #[cfg(feature = "http")] use crate::http::Http; -use crate::internal::prelude::*; use crate::model::prelude::*; /// A builder for creating a new [`CommandOption`]. @@ -320,35 +320,20 @@ impl<'a> CreateCommandOption<'a> { /// A builder for creating a new [`Command`]. /// -/// [`Self::name`] and [`Self::description`] are required fields. -/// /// [`Command`]: crate::model::application::Command /// /// Discord docs: -/// - [global command](https://discord.com/developers/docs/interactions/application-commands#create-global-application-command-json-params) -/// - [guild command](https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command-json-params) +/// - [global command](https://discord.com/developers/docs/interactions/application-commands#create-global-application-command) +/// - [guild command](https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command) #[derive(Clone, Debug, Serialize)] #[must_use] pub struct CreateCommand<'a> { - name: Cow<'a, str>, - name_localizations: HashMap, Cow<'a, str>>, - #[serde(skip_serializing_if = "Option::is_none")] - description: Option>, - description_localizations: HashMap, Cow<'a, str>>, - options: Cow<'a, [CreateCommandOption<'a>]>, - #[serde(skip_serializing_if = "Option::is_none")] - default_member_permissions: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[cfg(not(feature = "unstable"))] - dm_permission: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "type")] kind: Option, - #[serde(skip_serializing_if = "Option::is_none")] - integration_types: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - contexts: Option>, - nsfw: bool, + + #[serde(flatten)] + fields: EditCommand<'a>, } impl<'a> CreateCommand<'a> { @@ -356,20 +341,7 @@ impl<'a> CreateCommand<'a> { pub fn new(name: impl Into>) -> Self { Self { kind: None, - - name: name.into(), - name_localizations: HashMap::new(), - description: None, - description_localizations: HashMap::new(), - default_member_permissions: None, - #[cfg(not(feature = "unstable"))] - dm_permission: None, - - integration_types: None, - contexts: None, - - options: Cow::default(), - nsfw: false, + fields: EditCommand::new().name(name), } } @@ -380,15 +352,14 @@ impl<'a> CreateCommand<'a> { /// global commands of the same app cannot have the same name. Two guild-specific commands of /// the same app cannot have the same name. pub fn name(mut self, name: impl Into>) -> Self { - self.name = name.into(); + self.fields = self.fields.name(name); self } /// Specifies a localized name of the application command. /// /// ```rust - /// # serenity::builder::CreateCommand::new("") - /// .name("birthday") + /// # serenity::builder::CreateCommand::new("birthday") /// .name_localized("zh-CN", "生日") /// .name_localized("el", "γενέθλια") /// # ; @@ -398,7 +369,7 @@ impl<'a> CreateCommand<'a> { locale: impl Into>, name: impl Into>, ) -> Self { - self.name_localizations.insert(locale.into(), name.into()); + self.fields = self.fields.name_localized(locale, name); self } @@ -410,14 +381,14 @@ impl<'a> CreateCommand<'a> { /// Specifies the default permissions required to execute the command. pub fn default_member_permissions(mut self, permissions: Permissions) -> Self { - self.default_member_permissions = Some(permissions); + self.fields = self.fields.default_member_permissions(permissions); self } /// Specifies if the command is available in DMs. #[cfg(not(feature = "unstable"))] pub fn dm_permission(mut self, enabled: bool) -> Self { - self.dm_permission = Some(enabled); + self.fields = self.fields.dm_permission(enabled); self } @@ -425,14 +396,14 @@ impl<'a> CreateCommand<'a> { /// /// **Note**: Must be between 1 and 100 characters long. pub fn description(mut self, description: impl Into>) -> Self { - self.description = Some(description.into()); + self.fields = self.fields.description(description); self } /// Specifies a localized description of the application command. /// /// ```rust - /// # serenity::builder::CreateCommand::new("") + /// # serenity::builder::CreateCommand::new("birthday") /// .description("Wish a friend a happy birthday") /// .description_localized("zh-CN", "祝你朋友生日快乐") /// # ; @@ -442,7 +413,7 @@ impl<'a> CreateCommand<'a> { locale: impl Into>, description: impl Into>, ) -> Self { - self.description_localizations.insert(locale.into(), description.into()); + self.fields = self.fields.description_localized(locale, description); self } @@ -450,7 +421,7 @@ impl<'a> CreateCommand<'a> { /// /// **Note**: Application commands can have up to 25 options. pub fn add_option(mut self, option: CreateCommandOption<'a>) -> Self { - self.options.to_mut().push(option); + self.fields = self.fields.add_option(option); self } @@ -458,47 +429,45 @@ impl<'a> CreateCommand<'a> { /// /// **Note**: Application commands can have up to 25 options. pub fn set_options(mut self, options: impl Into]>>) -> Self { - self.options = options.into(); + self.fields = self.fields.set_options(options); self } /// Adds an installation context that this application command can be used in. pub fn add_integration_type(mut self, integration_type: InstallationContext) -> Self { - self.integration_types.get_or_insert_with(Vec::default).push(integration_type); + self.fields = self.fields.add_integration_type(integration_type); self } /// Sets the installation contexts that this application command can be used in. pub fn integration_types(mut self, integration_types: Vec) -> Self { - self.integration_types = Some(integration_types); + self.fields = self.fields.integration_types(integration_types); self } /// Adds an interaction context that this application command can be used in. pub fn add_context(mut self, context: InteractionContext) -> Self { - self.contexts.get_or_insert_with(Vec::default).push(context); + self.fields = self.fields.add_context(context); self } /// Sets the interaction contexts that this application command can be used in. pub fn contexts(mut self, contexts: Vec) -> Self { - self.contexts = Some(contexts); + self.fields = self.fields.contexts(contexts); self } /// Whether this command is marked NSFW (age-restricted) pub fn nsfw(mut self, nsfw: bool) -> Self { - self.nsfw = nsfw; + self.fields = self.fields.nsfw(nsfw); self } - /// Create a [`Command`], overriding an existing one with the same name if it exists. + /// Create a [`Command`], overwriting an existing one with the same name if it exists. /// /// Providing a [`GuildId`] will create a command in the corresponding [`Guild`]. Otherwise, a /// global command will be created. /// - /// Providing a [`CommandId`] will edit the corresponding command. - /// /// # Errors /// /// Returns [`Error::Http`] if invalid data is given. See [Discord's docs] for more details. @@ -507,19 +476,10 @@ impl<'a> CreateCommand<'a> { /// /// [Discord's docs]: https://discord.com/developers/docs/interactions/slash-commands #[cfg(feature = "http")] - pub async fn execute( - self, - http: &Http, - guild_id: Option, - command_id: Option, - ) -> Result { - match (guild_id, command_id) { - (Some(guild_id), Some(cmd_id)) => { - http.edit_guild_command(guild_id, cmd_id, &self).await - }, - (Some(guild_id), None) => http.create_guild_command(guild_id, &self).await, - (None, Some(cmd_id)) => http.edit_global_command(cmd_id, &self).await, - (None, None) => http.create_global_command(&self).await, + pub async fn execute(self, http: &Http, guild_id: Option) -> Result { + match guild_id { + Some(guild_id) => http.create_guild_command(guild_id, &self).await, + None => http.create_global_command(&self).await, } } } diff --git a/src/builder/edit_command.rs b/src/builder/edit_command.rs new file mode 100644 index 00000000000..4800074e1ae --- /dev/null +++ b/src/builder/edit_command.rs @@ -0,0 +1,179 @@ +use std::borrow::Cow; +use std::collections::HashMap; + +use crate::builder::CreateCommandOption; +#[cfg(feature = "http")] +use crate::http::Http; +use crate::model::prelude::*; + +/// A builder for editing an existing [`Command`]. +/// +/// [`Command`]: crate::model::application::Command +/// +/// Discord docs: +/// - [global command](https://discord.com/developers/docs/interactions/application-commands#edit-global-application-command) +/// - [guild command](https://discord.com/developers/docs/interactions/application-commands#edit-guild-application-command) +#[derive(Clone, Debug, Default, Serialize)] +#[must_use] +pub struct EditCommand<'a> { + name: Option>, + name_localizations: HashMap, Cow<'a, str>>, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option>, + description_localizations: HashMap, Cow<'a, str>>, + options: Cow<'a, [CreateCommandOption<'a>]>, + #[serde(skip_serializing_if = "Option::is_none")] + default_member_permissions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(not(feature = "unstable"))] + dm_permission: Option, + #[serde(skip_serializing_if = "Option::is_none")] + integration_types: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + contexts: Option>, + nsfw: bool, +} + +impl<'a> EditCommand<'a> { + /// Equivalent to [`Self::default`]. + pub fn new() -> Self { + Self::default() + } + + /// Specifies the name of the application command. + /// + /// **Note**: Must be between 1 and 32 lowercase characters, matching `r"^[\w-]{1,32}$"`. Two + /// global commands of the same app cannot have the same name. Two guild-specific commands of + /// the same app cannot have the same name. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + /// Specifies a localized name of the application command. + /// + /// ```rust + /// # serenity::builder::EditCommand::new() + /// .name("birthday") + /// .name_localized("zh-CN", "生日") + /// .name_localized("el", "γενέθλια") + /// # ; + /// ``` + pub fn name_localized( + mut self, + locale: impl Into>, + name: impl Into>, + ) -> Self { + self.name_localizations.insert(locale.into(), name.into()); + self + } + + /// Specifies the default permissions required to execute the command. + pub fn default_member_permissions(mut self, permissions: Permissions) -> Self { + self.default_member_permissions = Some(permissions); + self + } + + /// Specifies if the command is available in DMs. + #[cfg(not(feature = "unstable"))] + pub fn dm_permission(mut self, enabled: bool) -> Self { + self.dm_permission = Some(enabled); + self + } + + /// Specifies the description of the application command. + /// + /// **Note**: Must be between 1 and 100 characters long. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = Some(description.into()); + self + } + + /// Specifies a localized description of the application command. + /// + /// ```rust + /// # serenity::builder::CreateCommand::new("") + /// .description("Wish a friend a happy birthday") + /// .description_localized("zh-CN", "祝你朋友生日快乐") + /// # ; + /// ``` + pub fn description_localized( + mut self, + locale: impl Into>, + description: impl Into>, + ) -> Self { + self.description_localizations.insert(locale.into(), description.into()); + self + } + + /// Adds an application command option for the application command. + /// + /// **Note**: Application commands can have up to 25 options. + pub fn add_option(mut self, option: CreateCommandOption<'a>) -> Self { + self.options.to_mut().push(option); + self + } + + /// Sets all the application command options for the application command. + /// + /// **Note**: Application commands can have up to 25 options. + pub fn set_options(mut self, options: impl Into]>>) -> Self { + self.options = options.into(); + self + } + + /// Adds an installation context that this application command can be used in. + pub fn add_integration_type(mut self, integration_type: InstallationContext) -> Self { + self.integration_types.get_or_insert_with(Vec::default).push(integration_type); + self + } + + /// Sets the installation contexts that this application command can be used in. + pub fn integration_types(mut self, integration_types: Vec) -> Self { + self.integration_types = Some(integration_types); + self + } + + /// Adds an interaction context that this application command can be used in. + pub fn add_context(mut self, context: InteractionContext) -> Self { + self.contexts.get_or_insert_with(Vec::default).push(context); + self + } + + /// Sets the interaction contexts that this application command can be used in. + pub fn contexts(mut self, contexts: Vec) -> Self { + self.contexts = Some(contexts); + self + } + + /// Whether this command is marked NSFW (age-restricted) + pub fn nsfw(mut self, nsfw: bool) -> Self { + self.nsfw = nsfw; + self + } + + /// Edit a [`Command`], overwriting an existing one with the same name if it exists. + /// + /// Providing a [`GuildId`] will edit a command in the corresponding [`Guild`]. Otherwise, a + /// global command will be edited. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if invalid data is given. See [Discord's docs] for more details. + /// + /// May also return [`Error::Json`] if there is an error in deserializing the API response. + /// + /// [Discord's docs]: https://discord.com/developers/docs/interactions/slash-commands + #[cfg(feature = "http")] + pub async fn execute( + self, + http: &Http, + command_id: CommandId, + guild_id: Option, + ) -> Result { + match guild_id { + Some(guild_id) => http.edit_guild_command(guild_id, command_id, &self).await, + None => http.edit_global_command(command_id, &self).await, + } + } +} diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 8050f6718a9..d923cbcae63 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -58,6 +58,7 @@ mod create_thread; mod create_webhook; mod edit_automod_rule; mod edit_channel; +mod edit_command; mod edit_guild; mod edit_guild_welcome_screen; mod edit_guild_widget; @@ -100,6 +101,7 @@ pub use create_thread::*; pub use create_webhook::*; pub use edit_automod_rule::*; pub use edit_channel::*; +pub use edit_command::*; pub use edit_guild::*; pub use edit_guild_welcome_screen::*; pub use edit_guild_widget::*; diff --git a/src/model/application/command.rs b/src/model/application/command.rs index f2b2ec98b27..2599595e979 100644 --- a/src/model/application/command.rs +++ b/src/model/application/command.rs @@ -4,7 +4,7 @@ use serde::Serialize; use super::{InstallationContext, InteractionContext}; #[cfg(feature = "model")] -use crate::builder::CreateCommand; +use crate::builder::{CreateCommand, EditCommand}; #[cfg(feature = "model")] use crate::http::Http; use crate::model::prelude::*; @@ -141,7 +141,7 @@ impl Command { /// /// [`InteractionCreate`]: crate::gateway::client::EventHandler::interaction_create pub async fn create_global_command(http: &Http, builder: CreateCommand<'_>) -> Result { - builder.execute(http, None, None).await + builder.execute(http, None).await } /// Override all global application commands. @@ -164,9 +164,9 @@ impl Command { pub async fn edit_global_command( http: &Http, command_id: CommandId, - builder: CreateCommand<'_>, + builder: EditCommand<'_>, ) -> Result { - builder.execute(http, None, Some(command_id)).await + builder.execute(http, command_id, None).await } /// Gets all global commands. diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index 7637a2abb5b..dbefcc48076 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -13,6 +13,7 @@ use crate::builder::{ CreateScheduledEvent, CreateSticker, EditAutoModRule, + EditCommand, EditCommandPermissions, EditGuild, EditGuildWelcomeScreen, @@ -1434,7 +1435,7 @@ impl GuildId { /// /// See [`CreateCommand::execute`] for a list of possible errors. pub async fn create_command(self, http: &Http, builder: CreateCommand<'_>) -> Result { - builder.execute(http, Some(self), None).await + builder.execute(http, Some(self)).await } /// Override all guild application commands. @@ -1502,9 +1503,9 @@ impl GuildId { self, http: &Http, command_id: CommandId, - builder: CreateCommand<'_>, + builder: EditCommand<'_>, ) -> Result { - builder.execute(http, Some(self), Some(command_id)).await + builder.execute(http, command_id, Some(self)).await } /// Delete guild application command by its Id.