diff --git a/cache/in-memory/CHANGELOG.md b/cache/in-memory/CHANGELOG.md index 1bdb436ddcb..dcccdefc8ca 100644 --- a/cache/in-memory/CHANGELOG.md +++ b/cache/in-memory/CHANGELOG.md @@ -2,6 +2,16 @@ Changelog for `twilight-cache-inmemory`. +## [0.6.1] - 2021-08-30 + +### Additions + +Add `InMemoryCache::guild_integrations` to retrieve a guild's list of +integration IDs and `InMemoryCache::integration` to retrieve an integration by +guild and integration ID ([#1134] - [@zeylahellyer]). + +[#1134]: https://github.com/twilight-rs/twilight/pull/1134 + ## [0.6.0] - 2021-07-31 ### Changes @@ -398,6 +408,7 @@ Initial release. [#528]: https://github.com/twilight-rs/twilight/pull/528 [#524]: https://github.com/twilight-rs/twilight/pull/524 +[0.6.1]: https://github.com/twilight-rs/twilight/releases/tag/cache-in-memory-0.6.1 [0.5.3]: https://github.com/twilight-rs/twilight/releases/tag/cache-in-memory-0.5.3 [0.5.2]: https://github.com/twilight-rs/twilight/releases/tag/cache-in-memory-0.5.2 [0.5.1]: https://github.com/twilight-rs/twilight/releases/tag/cache-in-memory-0.5.1 diff --git a/cache/in-memory/Cargo.toml b/cache/in-memory/Cargo.toml index d7e4e670a4f..05cc42e8cf8 100644 --- a/cache/in-memory/Cargo.toml +++ b/cache/in-memory/Cargo.toml @@ -12,7 +12,7 @@ name = "twilight-cache-inmemory" publish = false repository = "https://github.com/twilight-rs/twilight" readme = "README.md" -version = "0.6.0" +version = "0.6.1" [dependencies] bitflags = { default-features = false, version = "1" } diff --git a/cache/in-memory/src/event/interaction.rs b/cache/in-memory/src/event/interaction.rs index daa64717951..acb96428bcb 100644 --- a/cache/in-memory/src/event/interaction.rs +++ b/cache/in-memory/src/event/interaction.rs @@ -62,8 +62,17 @@ mod tests { application_command::{CommandData, CommandInteractionDataResolved, InteractionMember}, ApplicationCommand, InteractionType, }, + channel::{ + message::{ + sticker::{MessageSticker, StickerFormatType, StickerId}, + MessageFlags, MessageType, + }, + Message, + }, guild::{PartialMember, Permissions, Role}, - id::{ApplicationId, ChannelId, CommandId, GuildId, InteractionId, RoleId, UserId}, + id::{ + ApplicationId, ChannelId, CommandId, GuildId, InteractionId, MessageId, RoleId, UserId, + }, user::User, }; @@ -80,15 +89,74 @@ mod tests { options: Vec::new(), resolved: Some(CommandInteractionDataResolved { channels: Vec::new(), - members: vec![InteractionMember { + members: Vec::from([InteractionMember { hoisted_role: None, id: UserId(7), joined_at: Some("joined at date".into()), nick: None, premium_since: None, roles: vec![RoleId(8)], - }], - roles: vec![Role { + }]), + messages: Vec::from([Message { + activity: None, + application: None, + application_id: None, + attachments: Vec::new(), + author: User { + accent_color: None, + avatar: Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned()), + banner: None, + bot: false, + discriminator: "0001".to_owned(), + email: None, + flags: None, + id: UserId(3), + locale: None, + mfa_enabled: None, + name: "test".to_owned(), + premium_type: None, + public_flags: None, + system: None, + verified: None, + }, + channel_id: ChannelId(2), + components: Vec::new(), + content: "ping".to_owned(), + edited_timestamp: None, + embeds: Vec::new(), + flags: Some(MessageFlags::empty()), + guild_id: Some(GuildId(1)), + id: MessageId(4), + interaction: None, + kind: MessageType::Regular, + member: Some(PartialMember { + deaf: false, + joined_at: Some("2020-01-01T00:00:00.000000+00:00".to_owned()), + mute: false, + nick: Some("member nick".to_owned()), + permissions: None, + premium_since: None, + roles: Vec::new(), + user: None, + }), + mention_channels: Vec::new(), + mention_everyone: false, + mention_roles: Vec::new(), + mentions: Vec::new(), + pinned: false, + reactions: Vec::new(), + reference: None, + sticker_items: vec![MessageSticker { + format_type: StickerFormatType::Png, + id: StickerId(1), + name: "sticker name".to_owned(), + }], + referenced_message: None, + timestamp: "2020-02-02T02:02:02.020000+00:00".to_owned(), + tts: false, + webhook_id: None, + }]), + roles: Vec::from([Role { color: 0u32, hoist: false, id: RoleId(8), @@ -98,8 +166,8 @@ mod tests { permissions: Permissions::empty(), position: 2i64, tags: None, - }], - users: vec![User { + }]), + users: Vec::from([User { accent_color: None, avatar: Some("different avatar".into()), banner: None, @@ -115,7 +183,7 @@ mod tests { public_flags: None, system: None, verified: None, - }], + }]), }), }, guild_id: Some(GuildId(3)), diff --git a/cache/in-memory/src/lib.rs b/cache/in-memory/src/lib.rs index 785b815a1f7..99a27093657 100644 --- a/cache/in-memory/src/lib.rs +++ b/cache/in-memory/src/lib.rs @@ -411,6 +411,16 @@ impl InMemoryCache { self.0.guild_emojis.get(&guild_id).map(|r| r.clone()) } + /// Gets the set of integrations in a guild. + /// + /// This requires the [`GUILD_INTEGRATIONS`] intent. The + /// [`ResourceType::INTEGRATION`] resource type must be enabled. + /// + /// [`GUILD_INTEGRATIONS`]: twilight_model::gateway::Intents::GUILD_INTEGRATIONS + pub fn guild_integrations(&self, guild_id: GuildId) -> Option> { + self.0.guild_integrations.get(&guild_id).map(|r| r.clone()) + } + /// Gets the set of members in a guild. /// /// This list may be incomplete if not all members have been cached. @@ -458,6 +468,23 @@ impl InMemoryCache { .map(|r| r.value().clone()) } + /// Gets an integration by guild ID and integration ID. + /// + /// This is an O(1) operation. This requires the [`GUILD_INTEGRATIONS`] + /// intent. The [`ResourceType::INTEGRATION`] resource type must be enabled. + /// + /// [`GUILD_INTEGRATIONS`]: twilight_model::gateway::Intents::GUILD_INTEGRATIONS + pub fn integration( + &self, + guild_id: GuildId, + integration_id: IntegrationId, + ) -> Option { + self.0 + .integrations + .get(&(guild_id, integration_id)) + .map(|r| r.data.clone()) + } + /// Gets a member by guild ID and user ID. /// /// This is an O(1) operation. This requires the [`GUILD_MEMBERS`] intent. diff --git a/gateway/CHANGELOG.md b/gateway/CHANGELOG.md index 96c31317a56..b55920f8beb 100644 --- a/gateway/CHANGELOG.md +++ b/gateway/CHANGELOG.md @@ -2,6 +2,17 @@ Changelog for `twilight-gateway`. +## [0.6.2] - 2021-08-30 + +### Enhancements + +Reduce the log level of shard resumes from INFO to DEBUG ([#1137] - [@vilgotf]). + +Fix two remaining intradoc links ([#1128] - [@zeylahellyer]). + +[#1137]: https://github.com/twilight-rs/twilight/pull/1137 +[#1128]: https://github.com/twilight-rs/twilight/pull/1128 + ## [0.6.1] - 2021-08-18 ### Enhancements @@ -547,6 +558,7 @@ Initial release. [#515]: https://github.com/twilight-rs/twilight/pull/515 [#512]: https://github.com/twilight-rs/twilight/pull/512 +[0.6.2]: https://github.com/twilight-rs/twilight/releases/tag/gateway-0.6.2 [0.6.1]: https://github.com/twilight-rs/twilight/releases/tag/gateway-0.6.1 [0.5.5]: https://github.com/twilight-rs/twilight/releases/tag/gateway-0.5.5 [0.5.4]: https://github.com/twilight-rs/twilight/releases/tag/gateway-0.5.4 diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index a7681d3df89..edd5d7de965 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -12,7 +12,7 @@ name = "twilight-gateway" publish = false readme = "README.md" repository = "https://github.com/twilight-rs/twilight.git" -version = "0.6.1" +version = "0.6.2" [dependencies] bitflags = { default-features = false, version = "1" } diff --git a/gateway/src/cluster/impl.rs b/gateway/src/cluster/impl.rs index 3dfb72180aa..47739001ab1 100644 --- a/gateway/src/cluster/impl.rs +++ b/gateway/src/cluster/impl.rs @@ -592,8 +592,6 @@ impl Cluster { /// /// Returns a [`ClusterCommandErrorType::ShardNonexistent`] error type if /// the provided shard ID does not exist in the cluster. - /// - /// [`SessionInactiveError`]: struct.SessionInactiveError.html pub async fn send(&self, id: u64, message: Message) -> Result<(), ClusterSendError> { let shard = self.shard(id).ok_or(ClusterSendError { kind: ClusterSendErrorType::ShardNonexistent { id }, diff --git a/gateway/src/cluster/scheme.rs b/gateway/src/cluster/scheme.rs index 824acde0620..23a900f2c64 100644 --- a/gateway/src/cluster/scheme.rs +++ b/gateway/src/cluster/scheme.rs @@ -164,7 +164,7 @@ impl Iterator for ShardSchemeIter { /// /// By default this is [`Auto`]. /// -/// [`Auto`]: #variant.Auto +/// [`Auto`]: Self::Auto #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum ShardScheme { diff --git a/gateway/src/shard/processor/impl.rs b/gateway/src/shard/processor/impl.rs index 07d0f5d6730..5d6a6d3885e 100644 --- a/gateway/src/shard/processor/impl.rs +++ b/gateway/src/shard/processor/impl.rs @@ -953,7 +953,7 @@ impl ShardProcessor { /// connection. async fn resume(&mut self) { #[cfg(feature = "tracing")] - tracing::info!("resuming shard {:?}", self.config.shard()); + tracing::debug!("resuming shard {:?}", self.config.shard()); self.session.set_stage(Stage::Resuming); self.session.stop_heartbeater(); diff --git a/http/CHANGELOG.md b/http/CHANGELOG.md index dbb1530a1ab..93556e21b3a 100644 --- a/http/CHANGELOG.md +++ b/http/CHANGELOG.md @@ -2,6 +2,41 @@ Changelog for `twilight-http`. +## [0.6.3] - 2021-08-30 + +### Additions + +Support message components, including action rows, buttons, and select menus +([#1020], [#1043], [#1044], [#1090], aggregate [#1121] - [@AEnterprise], +[@AsianIntel], [@zeylahellyer], [@7596ff]). + +Add comparing `StatusCode` with `u16` ([#1131] - [@zeylahellyer]). + +Add API error code 30040, described as "Maximum number of prune requests has +been reached. Try again later" ([#1125] - [@zeylahellyer]). + +### Enhancements + +Document that `tracing` is now disabled by default ([#1129] - [@zeylahellyer]). + +Add `Response>::model` and `Response::model` aliases +corresponding to their `models` equivalents ([#1123] - [@zeylahellyer]). + +Display body parsing errors as a legible string if they're UTF-8 valid +([#1118] - [@AEnterprise]). + +[#1131]: https://github.com/twilight-rs/twilight/pull/1131 +[#1129]: https://github.com/twilight-rs/twilight/pull/1129 +[#1125]: https://github.com/twilight-rs/twilight/pull/1125 +[#1123]: https://github.com/twilight-rs/twilight/pull/1123 +[#1121]: https://github.com/twilight-rs/twilight/pull/1121 +[#1120]: https://github.com/twilight-rs/twilight/pull/1120 +[#1118]: https://github.com/twilight-rs/twilight/pull/1118 +[#1090]: https://github.com/twilight-rs/twilight/pull/1090 +[#1044]: https://github.com/twilight-rs/twilight/pull/1044 +[#1043]: https://github.com/twilight-rs/twilight/pull/1043 +[#1020]: https://github.com/twilight-rs/twilight/pull/1020 + ## [0.6.2] - 2021-08-18 ### Additions @@ -1176,6 +1211,7 @@ Initial release. [0.2.0-beta.1:app integrations]: https://github.com/discord/discord-api-docs/commit/a926694e2f8605848bda6b57d21c8817559e5cec +[0.6.3]: https://github.com/twilight-rs/twilight/releases/tag/http-0.6.3 [0.6.2]: https://github.com/twilight-rs/twilight/releases/tag/http-0.6.2 [0.5.7]: https://github.com/twilight-rs/twilight/releases/tag/http-0.5.7 [0.5.6]: https://github.com/twilight-rs/twilight/releases/tag/http-0.5.6 diff --git a/http/Cargo.toml b/http/Cargo.toml index a28d3070775..cb6b05e9572 100644 --- a/http/Cargo.toml +++ b/http/Cargo.toml @@ -12,7 +12,7 @@ name = "twilight-http" publish = false readme = "README.md" repository = "https://github.com/twilight-rs/twilight.git" -version = "0.6.2" +version = "0.6.3" [dependencies] hyper = { default-features = false, features = ["client", "http1", "http2", "runtime"], version = "0.14" } diff --git a/http/README.md b/http/README.md index af4c72b41c3..d23e677a522 100644 --- a/http/README.md +++ b/http/README.md @@ -66,7 +66,7 @@ This is enabled by default. The `tracing` feature enables logging via the [`tracing`] crate. -This is enabled by default. +This is disabled by default. [`native-tls`]: https://crates.io/crates/native-tls [`hyper`]: https://crates.io/crates/hyper diff --git a/http/src/client/mod.rs b/http/src/client/mod.rs index 787c375b640..856a1bfd609 100644 --- a/http/src/client/mod.rs +++ b/http/src/client/mod.rs @@ -7,12 +7,20 @@ use crate::{ ratelimiting::Ratelimiter, request::{ application::{ - CreateFollowupMessage, CreateGlobalCommand, CreateGuildCommand, DeleteFollowupMessage, - DeleteGlobalCommand, DeleteGuildCommand, DeleteOriginalResponse, GetCommandPermissions, - GetGlobalCommands, GetGuildCommandPermissions, GetGuildCommands, GetOriginalResponse, - InteractionCallback, InteractionError, InteractionErrorType, SetCommandPermissions, - SetGlobalCommands, SetGuildCommands, UpdateCommandPermissions, UpdateFollowupMessage, - UpdateGlobalCommand, UpdateGuildCommand, UpdateOriginalResponse, + command::{ + create_global_command::CreateGlobalChatInputCommand, + create_guild_command::CreateGuildChatInputCommand, CreateGlobalCommand, + CreateGuildCommand, DeleteGlobalCommand, DeleteGuildCommand, GetCommandPermissions, + GetGlobalCommand, GetGlobalCommands, GetGuildCommand, GetGuildCommandPermissions, + GetGuildCommands, SetCommandPermissions, SetGlobalCommands, SetGuildCommands, + UpdateCommandPermissions, UpdateGlobalCommand, UpdateGuildCommand, + }, + interaction::{ + CreateFollowupMessage, DeleteFollowupMessage, DeleteOriginalResponse, + GetOriginalResponse, InteractionCallback, UpdateFollowupMessage, + UpdateOriginalResponse, + }, + InteractionError, InteractionErrorType, }, channel::{ reaction::delete_reaction::TargetUser, @@ -1838,10 +1846,9 @@ impl Client { )) } - /// Create a new command in a guild. + /// Create a new chat input command in a guild. /// - /// The name must be between 3 and 32 characters in length, and the - /// description must be between 1 and 100 characters in length. Creating a + /// The name must be between 1 and 32 characters in length. Creating a /// guild command with the same name as an already-existing guild command in /// the same guild will overwrite the old command. See [the discord docs] /// for more information. @@ -1853,24 +1860,77 @@ impl Client { /// [`Client::set_application_id`]. /// /// Returns an [`InteractionErrorType::CommandNameValidationFailed`] - /// error type if the command name is not between 3 and 32 characters. - /// - /// Returns an [`InteractionErrorType::CommandDescriptionValidationFailed`] - /// error type if the command description is not between 1 and - /// 100 characters. + /// error type if the command name is not between 1 and 32 characters. /// - /// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#create-guild-application-command + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command + #[deprecated( + note = "use `new_create_guild_command`, which does not require a description", + since = "0.6.4" + )] pub fn create_guild_command<'a>( &'a self, guild_id: GuildId, name: &'a str, description: &'a str, + ) -> Result, InteractionError> { + let application_id = self.application_id().ok_or(InteractionError { + kind: InteractionErrorType::ApplicationIdNotPresent, + })?; + + CreateGuildCommand::new(self, application_id, guild_id, name)?.chat_input(description) + } + + /// Create a new command in a guild. + /// + /// The name must be between 1 and 32 characters in length. Creating a + /// guild command with the same name as an already-existing guild command in + /// the same guild will overwrite the old command. See [the discord docs] + /// for more information. + /// + /// # Errors + /// + /// Returns an [`InteractionErrorType::ApplicationIdNotPresent`] + /// error type if an application ID has not been configured via + /// [`Client::set_application_id`]. + /// + /// Returns an [`InteractionErrorType::CommandNameValidationFailed`] + /// error type if the command name is not between 1 and 32 characters. + /// + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command + pub fn new_create_guild_command<'a>( + &'a self, + guild_id: GuildId, + name: &'a str, ) -> Result, InteractionError> { let application_id = self.application_id().ok_or(InteractionError { kind: InteractionErrorType::ApplicationIdNotPresent, })?; - CreateGuildCommand::new(self, application_id, guild_id, name, description) + CreateGuildCommand::new(self, application_id, guild_id, name) + } + + /// Fetch a guild command for your application. + /// + /// # Errors + /// + /// Returns an [`InteractionErrorType::ApplicationIdNotPresent`] + /// error type if an application ID has not been configured via + /// [`Client::set_application_id`]. + pub fn get_guild_command( + &self, + guild_id: GuildId, + command_id: CommandId, + ) -> Result, InteractionError> { + let application_id = self.application_id().ok_or(InteractionError { + kind: InteractionErrorType::ApplicationIdNotPresent, + })?; + + Ok(GetGuildCommand::new( + self, + application_id, + guild_id, + command_id, + )) } /// Fetch all commands for a guild, by ID. @@ -1902,7 +1962,7 @@ impl Client { /// error type if an application ID has not been configured via /// [`Client::set_application_id`]. /// - /// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#edit-guild-application-command + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#edit-guild-application-command pub fn update_guild_command( &self, guild_id: GuildId, @@ -1971,12 +2031,12 @@ impl Client { )) } - /// Create a new global command. + /// Create a new chat input global command. /// - /// The name must be between 3 and 32 characters in length, and the - /// description must be between 1 and 100 characters in length. Creating a - /// command with the same name as an already-existing global command will - /// overwrite the old command. See [the discord docs] for more information. + /// The name must be between 1 and 32 characters in length. The description + /// must be between 1 and 100 characters in length. Creating a command with + /// the same name as an already-existing global command will overwrite the + /// old command. See [the discord docs] for more information. /// /// # Errors /// @@ -1985,23 +2045,72 @@ impl Client { /// [`Client::set_application_id`]. /// /// Returns an [`InteractionErrorType::CommandNameValidationFailed`] - /// error type if the command name is not between 3 and 32 characters. + /// error type if the command name is not between 1 and 32 characters. /// /// Returns an [`InteractionErrorType::CommandDescriptionValidationFailed`] - /// error type if the command description is not between 1 and - /// 100 characters. - /// - /// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#create-global-application-command + /// error type if the command description is not between 1 and 100 + /// characters. + /// + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-global-application-command + #[deprecated( + note = "use `new_create_global_command`, which does not require a description", + since = "0.6.4" + )] pub fn create_global_command<'a>( &'a self, name: &'a str, description: &'a str, + ) -> Result, InteractionError> { + let application_id = self.application_id().ok_or(InteractionError { + kind: InteractionErrorType::ApplicationIdNotPresent, + })?; + + CreateGlobalCommand::new(self, application_id, name)?.chat_input(description) + } + + /// Create a new global command. + /// + /// The name must be between 1 and 32 characters in length. Creating a + /// command with the same name as an already-existing global command will + /// overwrite the old command. See [the discord docs] for more information. + /// + /// # Errors + /// + /// Returns an [`InteractionErrorType::ApplicationIdNotPresent`] + /// error type if an application ID has not been configured via + /// [`Client::set_application_id`]. + /// + /// Returns an [`InteractionErrorType::CommandNameValidationFailed`] + /// error type if the command name is not between 1 and 32 characters. + /// + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-global-application-command + pub fn new_create_global_command<'a>( + &'a self, + name: &'a str, ) -> Result, InteractionError> { let application_id = self.application_id().ok_or(InteractionError { kind: InteractionErrorType::ApplicationIdNotPresent, })?; - CreateGlobalCommand::new(self, application_id, name, description) + CreateGlobalCommand::new(self, application_id, name) + } + + /// Fetch a global command for your application. + /// + /// # Errors + /// + /// Returns an [`InteractionErrorType::ApplicationIdNotPresent`] + /// error type if an application ID has not been configured via + /// [`Client::set_application_id`]. + pub fn get_global_command( + &self, + command_id: CommandId, + ) -> Result, InteractionError> { + let application_id = self.application_id().ok_or(InteractionError { + kind: InteractionErrorType::ApplicationIdNotPresent, + })?; + + Ok(GetGlobalCommand::new(self, application_id, command_id)) } /// Fetch all global commands for your application. @@ -2030,7 +2139,7 @@ impl Client { /// error type if an application ID has not been configured via /// [`Client::set_application_id`]. /// - /// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#edit-global-application-command + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#edit-global-application-command pub fn update_global_command( &self, command_id: CommandId, diff --git a/http/src/lib.rs b/http/src/lib.rs index dc82cb3b86c..ebd72bb7d2d 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -64,7 +64,7 @@ //! //! The `tracing` feature enables logging via the [`tracing`] crate. //! -//! This is enabled by default. +//! This is disabled by default. //! //! [`native-tls`]: https://crates.io/crates/native-tls //! [`hyper`]: https://crates.io/crates/hyper diff --git a/http/src/request/application/create_global_command.rs b/http/src/request/application/command/create_global_command/chat_input.rs similarity index 79% rename from http/src/request/application/create_global_command.rs rename to http/src/request/application/command/create_global_command/chat_input.rs index a8d015c7a19..cd122f13aab 100644 --- a/http/src/request/application/create_global_command.rs +++ b/http/src/request/application/command/create_global_command/chat_input.rs @@ -1,4 +1,7 @@ -use super::{CommandBorrowed, InteractionError, InteractionErrorType}; +use super::super::{ + super::{InteractionError, InteractionErrorType}, + CommandBorrowed, +}; use crate::{ client::Client, error::Error as HttpError, @@ -7,20 +10,19 @@ use crate::{ routing::Route, }; use twilight_model::{ - application::command::{Command, CommandOption}, + application::command::{Command, CommandOption, CommandType}, id::ApplicationId, }; -/// Create a new global command. +/// Create a new chat input global command. /// -/// The name must be between 3 and 32 characters in length, and the description -/// must be between 1 and 100 characters in length. Creating a command with the -/// same name as an already-existing global command will overwrite the old -/// command. See [the discord docs] for more information. +/// The description must be between 1 and 100 characters in length. Creating a +/// command with the same name as an already-existing global command will +/// overwrite the old command. See [the discord docs] for more information. /// -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#create-global-application-command +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-global-application-command #[must_use = "requests must be configured and executed"] -pub struct CreateGlobalCommand<'a> { +pub struct CreateGlobalChatInputCommand<'a> { application_id: ApplicationId, default_permission: Option, description: &'a str, @@ -29,19 +31,13 @@ pub struct CreateGlobalCommand<'a> { options: Option<&'a [CommandOption]>, } -impl<'a> CreateGlobalCommand<'a> { +impl<'a> CreateGlobalChatInputCommand<'a> { pub(crate) fn new( http: &'a Client, application_id: ApplicationId, name: &'a str, description: &'a str, ) -> Result { - if !validate_inner::command_name(name) { - return Err(InteractionError { - kind: InteractionErrorType::CommandNameValidationFailed, - }); - } - if !validate_inner::command_description(&description) { return Err(InteractionError { kind: InteractionErrorType::CommandDescriptionValidationFailed, @@ -109,7 +105,8 @@ impl<'a> CreateGlobalCommand<'a> { .json(&CommandBorrowed { application_id: Some(self.application_id), default_permission: self.default_permission, - description: self.description, + description: Some(self.description), + kind: CommandType::ChatInput, name: self.name, options: self.options, }) diff --git a/http/src/request/application/command/create_global_command/message.rs b/http/src/request/application/command/create_global_command/message.rs new file mode 100644 index 00000000000..7ad550aeb28 --- /dev/null +++ b/http/src/request/application/command/create_global_command/message.rs @@ -0,0 +1,73 @@ +use super::super::CommandBorrowed; +use crate::{ + client::Client, + error::Error, + request::{Request, RequestBuilder}, + response::ResponseFuture, + routing::Route, +}; +use twilight_model::{ + application::command::{Command, CommandType}, + id::ApplicationId, +}; + +/// Create a new message global command. +/// +/// Creating a command with the same name as an already-existing global command +/// will overwrite the old command. See [the discord docs] for more information. +/// +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-global-application-command +#[must_use = "requests must be configured and executed"] +pub struct CreateGlobalMessageCommand<'a> { + application_id: ApplicationId, + default_permission: Option, + http: &'a Client, + name: &'a str, +} + +impl<'a> CreateGlobalMessageCommand<'a> { + pub(crate) const fn new( + http: &'a Client, + application_id: ApplicationId, + name: &'a str, + ) -> Self { + Self { + application_id, + default_permission: None, + http, + name, + } + } + + /// Whether the command is enabled by default when the app is added to a guild. + pub const fn default_permission(mut self, default: bool) -> Self { + self.default_permission = Some(default); + + self + } + + fn request(&self) -> Result { + Request::builder(&Route::CreateGlobalCommand { + application_id: self.application_id.0, + }) + .json(&CommandBorrowed { + application_id: Some(self.application_id), + default_permission: self.default_permission, + description: None, + kind: CommandType::Message, + name: self.name, + options: None, + }) + .map(RequestBuilder::build) + } + + /// Execute the request, returning a future resolving to a [`Response`]. + /// + /// [`Response`]: crate::response::Response + pub fn exec(self) -> ResponseFuture { + match self.request() { + Ok(request) => self.http.request(request), + Err(source) => ResponseFuture::error(source), + } + } +} diff --git a/http/src/request/application/command/create_global_command/mod.rs b/http/src/request/application/command/create_global_command/mod.rs new file mode 100644 index 00000000000..7d76795c2b5 --- /dev/null +++ b/http/src/request/application/command/create_global_command/mod.rs @@ -0,0 +1,88 @@ +mod chat_input; +mod message; +mod user; + +pub use self::{ + chat_input::CreateGlobalChatInputCommand, message::CreateGlobalMessageCommand, + user::CreateGlobalUserCommand, +}; + +use super::super::{InteractionError, InteractionErrorType}; +use crate::{request::validate_inner, Client}; +use twilight_model::id::ApplicationId; + +/// Create a new global command. +/// +/// The name must be between 1 and 32 characters in length. Creating a command +/// with the same name as an already-existing global command will overwrite the +/// old command. See [the discord docs] for more information. +/// +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-global-application-command +#[must_use = "the command must have a type"] +pub struct CreateGlobalCommand<'a> { + application_id: ApplicationId, + http: &'a Client, + name: &'a str, +} + +impl<'a> CreateGlobalCommand<'a> { + pub(crate) fn new( + http: &'a Client, + application_id: ApplicationId, + name: &'a str, + ) -> Result { + if !validate_inner::command_name(name) { + return Err(InteractionError { + kind: InteractionErrorType::CommandNameValidationFailed, + }); + } + + Ok(Self { + application_id, + http, + name, + }) + } + + /// Create a new chat input global command. + /// + /// The description must be between 1 and 100 characters in length. Creating + /// a command with the same name as an already-existing global command will + /// overwrite the old command. See [the discord docs] for more information. + /// + /// # Errors + /// + /// Returns an [`InteractionErrorType::CommandDescriptionValidationFailed`] + /// error type if the command description is not between 1 and + /// 100 characters. + /// + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-global-application-command + pub fn chat_input( + self, + description: &'a str, + ) -> Result, InteractionError> { + CreateGlobalChatInputCommand::new(self.http, self.application_id, self.name, description) + } + + /// Create a new message global command. + /// + /// Creating a command with the same name as an already-existing global + /// command will overwrite the old command. See [the discord docs] for more + /// information. + /// + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-global-application-command + pub const fn message(self) -> CreateGlobalMessageCommand<'a> { + CreateGlobalMessageCommand::new(self.http, self.application_id, self.name) + } + + /// Create a new user global command. + /// + /// Creating a command with the same name as an already-existing global + /// command will overwrite the old command. See [the discord docs] for more + /// information. + /// + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-global-application-command + pub const fn user(self) -> CreateGlobalUserCommand<'a> { + CreateGlobalUserCommand::new(self.http, self.application_id, self.name) + } +} diff --git a/http/src/request/application/command/create_global_command/user.rs b/http/src/request/application/command/create_global_command/user.rs new file mode 100644 index 00000000000..0fafb21e2d1 --- /dev/null +++ b/http/src/request/application/command/create_global_command/user.rs @@ -0,0 +1,73 @@ +use super::super::CommandBorrowed; +use crate::{ + client::Client, + error::Error, + request::{Request, RequestBuilder}, + response::ResponseFuture, + routing::Route, +}; +use twilight_model::{ + application::command::{Command, CommandType}, + id::ApplicationId, +}; + +/// Create a new user global command. +/// +/// Creating a command with the same name as an already-existing global command +/// will overwrite the old command. See [the discord docs] for more information. +/// +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-global-application-command +#[must_use = "requests must be configured and executed"] +pub struct CreateGlobalUserCommand<'a> { + application_id: ApplicationId, + default_permission: Option, + http: &'a Client, + name: &'a str, +} + +impl<'a> CreateGlobalUserCommand<'a> { + pub(crate) const fn new( + http: &'a Client, + application_id: ApplicationId, + name: &'a str, + ) -> Self { + Self { + application_id, + default_permission: None, + http, + name, + } + } + + /// Whether the command is enabled by default when the app is added to a guild. + pub const fn default_permission(mut self, default: bool) -> Self { + self.default_permission = Some(default); + + self + } + + fn request(&self) -> Result { + Request::builder(&Route::CreateGlobalCommand { + application_id: self.application_id.0, + }) + .json(&CommandBorrowed { + application_id: Some(self.application_id), + default_permission: self.default_permission, + description: None, + kind: CommandType::User, + name: self.name, + options: None, + }) + .map(RequestBuilder::build) + } + + /// Execute the request, returning a future resolving to a [`Response`]. + /// + /// [`Response`]: crate::response::Response + pub fn exec(self) -> ResponseFuture { + match self.request() { + Ok(request) => self.http.request(request), + Err(source) => ResponseFuture::error(source), + } + } +} diff --git a/http/src/request/application/create_guild_command.rs b/http/src/request/application/command/create_guild_command/chat_input.rs similarity index 79% rename from http/src/request/application/create_guild_command.rs rename to http/src/request/application/command/create_guild_command/chat_input.rs index 4e124682aa5..74b8d52d8d3 100644 --- a/http/src/request/application/create_guild_command.rs +++ b/http/src/request/application/command/create_guild_command/chat_input.rs @@ -1,4 +1,7 @@ -use super::{CommandBorrowed, InteractionError, InteractionErrorType}; +use super::super::{ + super::{InteractionError, InteractionErrorType}, + CommandBorrowed, +}; use crate::{ client::Client, error::Error as HttpError, @@ -7,20 +10,20 @@ use crate::{ routing::Route, }; use twilight_model::{ - application::command::{Command, CommandOption}, + application::command::{Command, CommandOption, CommandType}, id::{ApplicationId, GuildId}, }; -/// Create a new command in a guild. +/// Create a chat input command in a guild. /// -/// The name must be between 3 and 32 characters in length, and the description -/// must be between 1 and 100 characters in length. Creating a guild command -/// with the same name as an already-existing guild command in the same guild -/// will overwrite the old command. See [the discord docs] for more information. +/// The description must be between 1 and 100 characters in length. Creating a +/// guild command with the same name as an already-existing guild command in the +/// same guild will overwrite the old command. See [the discord docs] for more +/// information. /// -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#create-guild-application-command +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command #[must_use = "requests must be configured and executed"] -pub struct CreateGuildCommand<'a> { +pub struct CreateGuildChatInputCommand<'a> { application_id: ApplicationId, default_permission: Option, description: &'a str, @@ -30,7 +33,7 @@ pub struct CreateGuildCommand<'a> { options: Option<&'a [CommandOption]>, } -impl<'a> CreateGuildCommand<'a> { +impl<'a> CreateGuildChatInputCommand<'a> { pub(crate) fn new( http: &'a Client, application_id: ApplicationId, @@ -38,12 +41,6 @@ impl<'a> CreateGuildCommand<'a> { name: &'a str, description: &'a str, ) -> Result { - if !validate_inner::command_name(&name) { - return Err(InteractionError { - kind: InteractionErrorType::CommandNameValidationFailed, - }); - } - if !validate_inner::command_description(&description) { return Err(InteractionError { kind: InteractionErrorType::CommandDescriptionValidationFailed, @@ -114,7 +111,8 @@ impl<'a> CreateGuildCommand<'a> { .json(&CommandBorrowed { application_id: Some(self.application_id), default_permission: self.default_permission, - description: self.description, + description: Some(self.description), + kind: CommandType::ChatInput, name: self.name, options: self.options, }) diff --git a/http/src/request/application/command/create_guild_command/message.rs b/http/src/request/application/command/create_guild_command/message.rs new file mode 100644 index 00000000000..f755838b869 --- /dev/null +++ b/http/src/request/application/command/create_guild_command/message.rs @@ -0,0 +1,79 @@ +use super::super::CommandBorrowed; +use crate::{ + client::Client, + error::Error, + request::{Request, RequestBuilder}, + response::ResponseFuture, + routing::Route, +}; +use twilight_model::{ + application::command::{Command, CommandType}, + id::{ApplicationId, GuildId}, +}; + +/// Create a message command in a guild. +/// +/// Creating a guild command with the same name as an already-existing guild +/// command in the same guild will overwrite the old command. See [the discord +/// docs] for more information. +/// +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command +#[must_use = "requests must be configured and executed"] +pub struct CreateGuildMessageCommand<'a> { + application_id: ApplicationId, + default_permission: Option, + guild_id: GuildId, + http: &'a Client, + name: &'a str, +} + +impl<'a> CreateGuildMessageCommand<'a> { + pub(crate) const fn new( + http: &'a Client, + application_id: ApplicationId, + guild_id: GuildId, + name: &'a str, + ) -> Self { + Self { + application_id, + default_permission: None, + guild_id, + http, + name, + } + } + + /// Whether the command is enabled by default when the app is added to a + /// guild. + pub const fn default_permission(mut self, default: bool) -> Self { + self.default_permission = Some(default); + + self + } + + fn request(&self) -> Result { + Request::builder(&Route::CreateGuildCommand { + application_id: self.application_id.0, + guild_id: self.guild_id.0, + }) + .json(&CommandBorrowed { + application_id: Some(self.application_id), + default_permission: self.default_permission, + description: None, + kind: CommandType::Message, + name: self.name, + options: None, + }) + .map(RequestBuilder::build) + } + + /// Execute the request, returning a future resolving to a [`Response`]. + /// + /// [`Response`]: crate::response::Response + pub fn exec(self) -> ResponseFuture { + match self.request() { + Ok(request) => self.http.request(request), + Err(source) => ResponseFuture::error(source), + } + } +} diff --git a/http/src/request/application/command/create_guild_command/mod.rs b/http/src/request/application/command/create_guild_command/mod.rs new file mode 100644 index 00000000000..9a870d623d1 --- /dev/null +++ b/http/src/request/application/command/create_guild_command/mod.rs @@ -0,0 +1,99 @@ +mod chat_input; +mod message; +mod user; + +pub use self::{ + chat_input::CreateGuildChatInputCommand, message::CreateGuildMessageCommand, + user::CreateGuildUserCommand, +}; + +use super::super::{InteractionError, InteractionErrorType}; +use crate::{request::validate_inner, Client}; +use twilight_model::id::{ApplicationId, GuildId}; + +/// Create a new command in a guild. +/// +/// The name must be between 1 and 32 characters in length. Creating a guild +/// command with the same name as an already-existing guild command in the same +/// guild will overwrite the old command. See [the discord docs] for more +/// information. +/// +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command +#[must_use = "the command must have a type"] +pub struct CreateGuildCommand<'a> { + application_id: ApplicationId, + guild_id: GuildId, + http: &'a Client, + name: &'a str, +} + +impl<'a> CreateGuildCommand<'a> { + pub(crate) fn new( + http: &'a Client, + application_id: ApplicationId, + guild_id: GuildId, + name: &'a str, + ) -> Result { + if !validate_inner::command_name(name) { + return Err(InteractionError { + kind: InteractionErrorType::CommandNameValidationFailed, + }); + } + + Ok(Self { + application_id, + guild_id, + http, + name, + }) + } + + /// Create a chat input command in a guild. + /// + /// The description must be between 1 and 100 characters in length. Creating + /// a guild command with the same name as an already-existing guild command + /// in the same guild will overwrite the old command. See [the discord docs] + /// for more information. + /// + /// # Errors + /// + /// Returns an [`InteractionErrorType::CommandDescriptionValidationFailed`] + /// error type if the command description is not between 1 and + /// 100 characters. + /// + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command + pub fn chat_input( + self, + description: &'a str, + ) -> Result, InteractionError> { + CreateGuildChatInputCommand::new( + self.http, + self.application_id, + self.guild_id, + self.name, + description, + ) + } + + /// Create a message command in a guild. + /// + /// Creating a guild command with the same name as an already-existing guild + /// command in the same guild will overwrite the old command. See [the + /// discord docs] for more information. + /// + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command + pub const fn message(self) -> CreateGuildMessageCommand<'a> { + CreateGuildMessageCommand::new(self.http, self.application_id, self.guild_id, self.name) + } + + /// Create a user command in a guild. + /// + /// Creating a guild command with the same name as an already-existing guild + /// command in the same guild will overwrite the old command. See [the + /// discord docs] for more information. + /// + /// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command + pub const fn user(self) -> CreateGuildUserCommand<'a> { + CreateGuildUserCommand::new(self.http, self.application_id, self.guild_id, self.name) + } +} diff --git a/http/src/request/application/command/create_guild_command/user.rs b/http/src/request/application/command/create_guild_command/user.rs new file mode 100644 index 00000000000..be1c57d7b20 --- /dev/null +++ b/http/src/request/application/command/create_guild_command/user.rs @@ -0,0 +1,78 @@ +use super::super::CommandBorrowed; +use crate::{ + client::Client, + error::Error, + request::{Request, RequestBuilder}, + response::ResponseFuture, + routing::Route, +}; +use twilight_model::{ + application::command::{Command, CommandType}, + id::{ApplicationId, GuildId}, +}; + +/// Create a user command in a guild. +/// +/// Creating a guild command with the same name as an already-existing guild +/// command in the same guild will overwrite the old command. See [the discord +/// docs] for more information. +/// +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command +#[must_use = "requests must be configured and executed"] +pub struct CreateGuildUserCommand<'a> { + application_id: ApplicationId, + default_permission: Option, + guild_id: GuildId, + http: &'a Client, + name: &'a str, +} + +impl<'a> CreateGuildUserCommand<'a> { + pub(crate) const fn new( + http: &'a Client, + application_id: ApplicationId, + guild_id: GuildId, + name: &'a str, + ) -> Self { + Self { + application_id, + default_permission: None, + guild_id, + http, + name, + } + } + + /// Whether the command is enabled by default when the app is added to a guild. + pub const fn default_permission(mut self, default: bool) -> Self { + self.default_permission = Some(default); + + self + } + + fn request(&self) -> Result { + Request::builder(&Route::CreateGuildCommand { + application_id: self.application_id.0, + guild_id: self.guild_id.0, + }) + .json(&CommandBorrowed { + application_id: Some(self.application_id), + default_permission: self.default_permission, + description: None, + kind: CommandType::User, + name: self.name, + options: None, + }) + .map(RequestBuilder::build) + } + + /// Execute the request, returning a future resolving to a [`Response`]. + /// + /// [`Response`]: crate::response::Response + pub fn exec(self) -> ResponseFuture { + match self.request() { + Ok(request) => self.http.request(request), + Err(source) => ResponseFuture::error(source), + } + } +} diff --git a/http/src/request/application/delete_global_command.rs b/http/src/request/application/command/delete_global_command.rs similarity index 100% rename from http/src/request/application/delete_global_command.rs rename to http/src/request/application/command/delete_global_command.rs diff --git a/http/src/request/application/delete_guild_command.rs b/http/src/request/application/command/delete_guild_command.rs similarity index 100% rename from http/src/request/application/delete_guild_command.rs rename to http/src/request/application/command/delete_guild_command.rs diff --git a/http/src/request/application/get_command_permissions.rs b/http/src/request/application/command/get_command_permissions.rs similarity index 100% rename from http/src/request/application/get_command_permissions.rs rename to http/src/request/application/command/get_command_permissions.rs diff --git a/http/src/request/application/command/get_global_command.rs b/http/src/request/application/command/get_global_command.rs new file mode 100644 index 00000000000..d0649453445 --- /dev/null +++ b/http/src/request/application/command/get_global_command.rs @@ -0,0 +1,39 @@ +use crate::{client::Client, request::Request, response::ResponseFuture, routing::Route}; +use twilight_model::{ + application::command::Command, + id::{ApplicationId, CommandId}, +}; + +/// Retrieve a global command for an application. +#[must_use = "requests must be configured and executed"] +pub struct GetGlobalCommand<'a> { + application_id: ApplicationId, + command_id: CommandId, + http: &'a Client, +} + +impl<'a> GetGlobalCommand<'a> { + pub(crate) const fn new( + http: &'a Client, + application_id: ApplicationId, + command_id: CommandId, + ) -> Self { + Self { + application_id, + command_id, + http, + } + } + + /// Execute the request, returning a future resolving to a [`Response`]. + /// + /// [`Response`]: crate::response::Response + pub fn exec(self) -> ResponseFuture { + let request = Request::from_route(&Route::GetGlobalCommand { + application_id: self.application_id.0, + command_id: self.command_id.0, + }); + + self.http.request(request) + } +} diff --git a/http/src/request/application/get_global_commands.rs b/http/src/request/application/command/get_global_commands.rs similarity index 100% rename from http/src/request/application/get_global_commands.rs rename to http/src/request/application/command/get_global_commands.rs diff --git a/http/src/request/application/command/get_guild_command.rs b/http/src/request/application/command/get_guild_command.rs new file mode 100644 index 00000000000..43ec3de3f4f --- /dev/null +++ b/http/src/request/application/command/get_guild_command.rs @@ -0,0 +1,43 @@ +use crate::{client::Client, request::Request, response::ResponseFuture, routing::Route}; +use twilight_model::{ + application::command::Command, + id::{ApplicationId, CommandId, GuildId}, +}; + +/// Retrieve a global command for an application. +#[must_use = "requests must be configured and executed"] +pub struct GetGuildCommand<'a> { + application_id: ApplicationId, + command_id: CommandId, + guild_id: GuildId, + http: &'a Client, +} + +impl<'a> GetGuildCommand<'a> { + pub(crate) const fn new( + http: &'a Client, + application_id: ApplicationId, + guild_id: GuildId, + command_id: CommandId, + ) -> Self { + Self { + application_id, + command_id, + guild_id, + http, + } + } + + /// Execute the request, returning a future resolving to a [`Response`]. + /// + /// [`Response`]: crate::response::Response + pub fn exec(self) -> ResponseFuture { + let request = Request::from_route(&Route::GetGuildCommand { + application_id: self.application_id.0, + command_id: self.command_id.0, + guild_id: self.guild_id.0, + }); + + self.http.request(request) + } +} diff --git a/http/src/request/application/get_guild_command_permissions.rs b/http/src/request/application/command/get_guild_command_permissions.rs similarity index 100% rename from http/src/request/application/get_guild_command_permissions.rs rename to http/src/request/application/command/get_guild_command_permissions.rs diff --git a/http/src/request/application/get_guild_commands.rs b/http/src/request/application/command/get_guild_commands.rs similarity index 100% rename from http/src/request/application/get_guild_commands.rs rename to http/src/request/application/command/get_guild_commands.rs diff --git a/http/src/request/application/command/mod.rs b/http/src/request/application/command/mod.rs new file mode 100644 index 00000000000..56627a20987 --- /dev/null +++ b/http/src/request/application/command/mod.rs @@ -0,0 +1,93 @@ +pub mod create_global_command; +pub mod create_guild_command; + +mod delete_global_command; +mod delete_guild_command; +mod get_command_permissions; +mod get_global_command; +mod get_global_commands; +mod get_guild_command; +mod get_guild_command_permissions; +mod get_guild_commands; +mod set_command_permissions; +mod set_global_commands; +mod set_guild_commands; +mod update_command_permissions; +mod update_global_command; +mod update_guild_command; + +pub use self::{ + create_global_command::CreateGlobalCommand, create_guild_command::CreateGuildCommand, + delete_global_command::DeleteGlobalCommand, delete_guild_command::DeleteGuildCommand, + get_command_permissions::GetCommandPermissions, get_global_command::GetGlobalCommand, + get_global_commands::GetGlobalCommands, get_guild_command::GetGuildCommand, + get_guild_command_permissions::GetGuildCommandPermissions, + get_guild_commands::GetGuildCommands, set_command_permissions::SetCommandPermissions, + set_global_commands::SetGlobalCommands, set_guild_commands::SetGuildCommands, + update_command_permissions::UpdateCommandPermissions, + update_global_command::UpdateGlobalCommand, update_guild_command::UpdateGuildCommand, +}; + +use serde::Serialize; +use twilight_model::{ + application::command::{CommandOption, CommandType}, + id::ApplicationId, +}; + +/// Version of [`Command`] but with borrowed fields. +/// +/// [`Command`]: twilight_model::application::command::Command +#[derive(Serialize)] +struct CommandBorrowed<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + pub application_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_permission: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option<&'a str>, + pub kind: CommandType, + pub name: &'a str, + #[serde(default)] + pub options: Option<&'a [CommandOption]>, +} + +#[cfg(test)] +mod tests { + use super::CommandBorrowed; + use twilight_model::{ + application::command::{BaseCommandOptionData, Command, CommandOption, CommandType}, + id::{ApplicationId, CommandId, GuildId}, + }; + + /// Test to convert a `Command` to a `CommandBorrowed`. + /// + /// Notably the point of this is to ensure that if a field is added to + /// `Command` or a type is changed then the destructure of it and creation + /// of `CommandBorrowed` will fail. + #[test] + fn test_command_borrowed_from_command() { + let command = Command { + application_id: Some(ApplicationId(1)), + default_permission: Some(true), + description: "command description".to_owned(), + guild_id: Some(GuildId(2)), + kind: CommandType::ChatInput, + name: "command name".to_owned(), + id: Some(CommandId(3)), + options: Vec::from([CommandOption::Boolean(BaseCommandOptionData { + description: "command description".to_owned(), + name: "command name".to_owned(), + required: true, + })]), + }; + + let _ = CommandBorrowed { + application_id: command.application_id, + default_permission: command.default_permission, + description: Some(&command.description), + kind: CommandType::ChatInput, + name: &command.name, + options: Some(&command.options), + }; + } +} diff --git a/http/src/request/application/command/set_command_permissions.rs b/http/src/request/application/command/set_command_permissions.rs new file mode 100644 index 00000000000..72a4a9b0592 --- /dev/null +++ b/http/src/request/application/command/set_command_permissions.rs @@ -0,0 +1,367 @@ +use crate::{ + client::Client, + error::Error, + request::{ + application::{InteractionError, InteractionErrorType}, + validate_inner, Request, RequestBuilder, + }, + response::ResponseFuture, + routing::Route, +}; +use serde::{ser::SerializeSeq, Serialize, Serializer}; +use twilight_model::{ + application::command::permissions::CommandPermissions, + id::{ApplicationId, CommandId, GuildId}, +}; + +#[derive(Clone, Copy, Debug)] +struct OptionalCommandPermissions<'a>( + [Option<&'a CommandPermissions>; InteractionError::GUILD_COMMAND_PERMISSION_LIMIT], +); + +impl OptionalCommandPermissions<'_> { + /// Create a new list of command permissions with `None` elements. + const fn new() -> Self { + Self([None; InteractionError::GUILD_COMMAND_PERMISSION_LIMIT]) + } + + /// Determine the number of elements present. + /// + /// If all elements are present then + /// [`InteractionError::GUILD_COMMAND_PERMISSION_LIMIT`] is returned. + /// + /// If no elements are present then 0 is returned. + fn amount_present(&self) -> usize { + // Iterate over the elements until we find one that is None. If we don't, + // then the maximum number are present. + self.0 + .iter() + .position(Option::is_none) + .unwrap_or(InteractionError::GUILD_COMMAND_PERMISSION_LIMIT) + } +} + +impl Serialize for OptionalCommandPermissions<'_> { + fn serialize(&self, serializer: S) -> Result { + let mut seq = serializer.serialize_seq(Some(self.amount_present()))?; + + let mut iter = self.0.iter(); + + // If an element isn't present while we haven't reached the end of the + // iterator then any trailing elements aren't present either. + while let Some(Some(value)) = iter.next() { + seq.serialize_element(value)?; + } + + seq.end() + } +} + +/// A sorted command's permissions. +/// +/// Used in combination with [`SortedCommands`]. +#[derive(Clone, Copy, Debug, Serialize)] +struct SortedCommand<'a> { + #[serde(skip_serializing)] + count: u8, + id: CommandId, + permissions: OptionalCommandPermissions<'a>, +} + +impl SortedCommand<'_> { + /// Create a new default sorted command with no configured permissions. + /// + /// The ID of the command is `u64::MAX`. + const fn new() -> Self { + Self { + count: 0, + id: CommandId(u64::MAX), + permissions: OptionalCommandPermissions::new(), + } + } + + // Retrieve the current count as a usize for indexing. + const fn count(self) -> usize { + self.count as usize + } +} + +/// Sorted list of commands and their permissions. +#[derive(Debug)] +struct SortedCommands<'a> { + inner: [SortedCommand<'a>; InteractionError::GUILD_COMMAND_LIMIT], +} + +impl<'a> SortedCommands<'a> { + pub const fn from_pairs( + pairs: &'a [(CommandId, CommandPermissions)], + ) -> Result { + let mut sorted = [SortedCommand::new(); InteractionError::GUILD_COMMAND_LIMIT]; + let mut outer_idx = 0; + + 'outer: while outer_idx < pairs.len() { + let (command_id, permissions) = &pairs[outer_idx]; + let mut inner_idx = 0; + + while inner_idx < sorted.len() { + // If the sorted command ID is neither the currently iterated + // provided command ID nor the maximum value, then we know this + // isn't it and can't be used. + let sorted_id = sorted[inner_idx].id; + + if sorted_id.0 != command_id.0 && sorted_id.0 != u64::MAX { + inner_idx += 1; + + continue; + } + + // We've got the right sorted command, but we first need to check + // if we've already reached the maximum number of command + // permissions allowed. + let sorted_count = sorted[inner_idx].count(); + + if !validate_inner::guild_command_permissions(sorted_count + 1) { + return Err(InteractionError { + kind: InteractionErrorType::TooManyCommandPermissions, + }); + } + + // Set the sorted command's ID if it's currently the maximum + // value. + if sorted_id.0 != command_id.0 { + sorted[inner_idx].id = *command_id; + } + + // And now set the permissions and increment the number of + // permissions set. + sorted[inner_idx].permissions.0[sorted_count] = Some(permissions); + sorted[inner_idx].count += 1; + + outer_idx += 1; + + continue 'outer; + } + + // We've run out of space in the sorted permissions, which means the + // user provided too many commands. + return Err(InteractionError { + kind: InteractionErrorType::TooManyCommands, + }); + } + + Ok(Self { inner: sorted }) + } +} + +impl Serialize for SortedCommands<'_> { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_seq(self.inner.iter().filter(|item| item.id.0 != u64::MAX)) + } +} + +/// Update command permissions for all commands in a guild. +/// +/// This overwrites the command permissions so the full set of permissions +/// have to be sent every time. +#[derive(Debug)] +#[must_use = "requests must be configured and executed"] +pub struct SetCommandPermissions<'a> { + application_id: ApplicationId, + guild_id: GuildId, + http: &'a Client, + permissions: SortedCommands<'a>, +} + +impl<'a> SetCommandPermissions<'a> { + pub(crate) const fn new( + http: &'a Client, + application_id: ApplicationId, + guild_id: GuildId, + permissions: &'a [(CommandId, CommandPermissions)], + ) -> Result { + let sorted_permissions = match SortedCommands::from_pairs(permissions) { + Ok(sorted_permissions) => sorted_permissions, + Err(source) => return Err(source), + }; + + Ok(Self { + application_id, + guild_id, + http, + permissions: sorted_permissions, + }) + } + + fn request(&self) -> Result { + Request::builder(&Route::SetCommandPermissions { + application_id: self.application_id.0, + guild_id: self.guild_id.0, + }) + .json(&self.permissions) + .map(RequestBuilder::build) + } + + /// Execute the request, returning a future resolving to a [`Response`]. + /// + /// [`Response`]: crate::response::Response + pub fn exec(self) -> ResponseFuture { + match self.request() { + Ok(request) => self.http.request(request), + Err(source) => ResponseFuture::error(source), + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + super::super::{InteractionError, InteractionErrorType}, + SetCommandPermissions, + }; + use crate::Client; + use serde::Deserialize; + use std::{error::Error, iter}; + use twilight_model::{ + application::command::permissions::{CommandPermissions, CommandPermissionsType}, + id::{ApplicationId, CommandId, GuildId, RoleId}, + }; + + const APPLICATION_ID: ApplicationId = ApplicationId(1); + const GUILD_ID: GuildId = GuildId(2); + + #[derive(Debug, Deserialize, Eq, PartialEq)] + struct GuildCommandPermissionDeserializable { + id: CommandId, + permissions: Vec, + } + + fn command_permissions(id: CommandId) -> impl Iterator { + iter::repeat(( + id, + CommandPermissions { + id: CommandPermissionsType::Role(RoleId(4)), + permission: true, + }, + )) + } + + #[allow(unused)] + #[test] + fn test_correct_validation() -> Result<(), Box> { + let http = Client::new("token".to_owned()); + let command_permissions = &[ + ( + CommandId(1), + CommandPermissions { + id: CommandPermissionsType::Role(RoleId(3)), + permission: true, + }, + ), + ( + CommandId(1), + CommandPermissions { + id: CommandPermissionsType::Role(RoleId(4)), + permission: true, + }, + ), + ( + CommandId(2), + CommandPermissions { + id: CommandPermissionsType::Role(RoleId(5)), + permission: true, + }, + ), + ]; + + let builder = + SetCommandPermissions::new(&http, APPLICATION_ID, GUILD_ID, command_permissions)?; + + let request = builder.request()?; + let body = request.body().expect("body must be present"); + let actual = serde_json::from_slice::>(body)?; + + let expected = &[ + GuildCommandPermissionDeserializable { + id: CommandId(1), + permissions: Vec::from([ + CommandPermissions { + id: CommandPermissionsType::Role(RoleId(3)), + permission: true, + }, + CommandPermissions { + id: CommandPermissionsType::Role(RoleId(4)), + permission: true, + }, + ]), + }, + GuildCommandPermissionDeserializable { + id: CommandId(2), + permissions: Vec::from([CommandPermissions { + id: CommandPermissionsType::Role(RoleId(5)), + permission: true, + }]), + }, + ]; + + assert_eq!(expected, actual.as_slice()); + + Ok(()) + } + + #[test] + fn test_incorrect_validation() { + let http = Client::new("token".to_owned()); + let command_permissions = command_permissions(CommandId(2)) + .take(InteractionError::GUILD_COMMAND_PERMISSION_LIMIT + 1) + .collect::>(); + + let request = + SetCommandPermissions::new(&http, APPLICATION_ID, GUILD_ID, &command_permissions); + assert!(matches!( + request.unwrap_err().kind(), + InteractionErrorType::TooManyCommandPermissions + )); + } + + #[test] + fn test_limits() { + const SIZE: usize = InteractionError::GUILD_COMMAND_LIMIT; + + let http = Client::new("token".to_owned()); + let command_permissions = (1..=SIZE) + .flat_map(|id| { + command_permissions(CommandId(id as u64)) + .take(InteractionError::GUILD_COMMAND_PERMISSION_LIMIT) + }) + .collect::>(); + + assert!( + SetCommandPermissions::new(&http, APPLICATION_ID, GUILD_ID, &command_permissions) + .is_ok() + ); + } + + #[test] + fn test_command_count_over_limit() { + const SIZE: usize = 101; + + let http = Client::new("token".to_owned()); + let command_permissions = (1..=SIZE) + .flat_map(|id| command_permissions(CommandId(id as u64)).take(3)) + .collect::>(); + + let request = + SetCommandPermissions::new(&http, APPLICATION_ID, GUILD_ID, &command_permissions); + assert!(matches!( + request.unwrap_err().kind(), + InteractionErrorType::TooManyCommands + )); + } + + #[test] + fn test_no_permissions() { + let http = Client::new("token".to_owned()); + + assert!(SetCommandPermissions::new(&http, APPLICATION_ID, GUILD_ID, &[]).is_ok()); + } +} diff --git a/http/src/request/application/set_global_commands.rs b/http/src/request/application/command/set_global_commands.rs similarity index 100% rename from http/src/request/application/set_global_commands.rs rename to http/src/request/application/command/set_global_commands.rs diff --git a/http/src/request/application/set_guild_commands.rs b/http/src/request/application/command/set_guild_commands.rs similarity index 100% rename from http/src/request/application/set_guild_commands.rs rename to http/src/request/application/command/set_guild_commands.rs diff --git a/http/src/request/application/update_command_permissions.rs b/http/src/request/application/command/update_command_permissions.rs similarity index 100% rename from http/src/request/application/update_command_permissions.rs rename to http/src/request/application/command/update_command_permissions.rs diff --git a/http/src/request/application/update_global_command.rs b/http/src/request/application/command/update_global_command.rs similarity index 97% rename from http/src/request/application/update_global_command.rs rename to http/src/request/application/command/update_global_command.rs index c7c695a0325..c9dda8300e1 100644 --- a/http/src/request/application/update_global_command.rs +++ b/http/src/request/application/command/update_global_command.rs @@ -26,7 +26,7 @@ struct UpdateGlobalCommandFields<'a> { /// You must specify a name and description. See [the discord docs] for more /// information. /// -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#edit-global-application-command +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#edit-global-application-command #[must_use = "requests must be configured and executed"] pub struct UpdateGlobalCommand<'a> { fields: UpdateGlobalCommandFields<'a>, diff --git a/http/src/request/application/update_guild_command.rs b/http/src/request/application/command/update_guild_command.rs similarity index 97% rename from http/src/request/application/update_guild_command.rs rename to http/src/request/application/command/update_guild_command.rs index 37f4b4e0314..1807eee8e79 100644 --- a/http/src/request/application/update_guild_command.rs +++ b/http/src/request/application/command/update_guild_command.rs @@ -26,7 +26,7 @@ struct UpdateGuildCommandFields<'a> { /// You must specify a name and description. See [the discord docs] for more /// information. /// -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#edit-guild-application-command +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#edit-guild-application-command #[must_use = "requests must be configured and executed"] pub struct UpdateGuildCommand<'a> { fields: UpdateGuildCommandFields<'a>, diff --git a/http/src/request/application/create_followup_message.rs b/http/src/request/application/interaction/create_followup_message.rs similarity index 100% rename from http/src/request/application/create_followup_message.rs rename to http/src/request/application/interaction/create_followup_message.rs diff --git a/http/src/request/application/delete_followup_message.rs b/http/src/request/application/interaction/delete_followup_message.rs similarity index 100% rename from http/src/request/application/delete_followup_message.rs rename to http/src/request/application/interaction/delete_followup_message.rs diff --git a/http/src/request/application/delete_original_response.rs b/http/src/request/application/interaction/delete_original_response.rs similarity index 100% rename from http/src/request/application/delete_original_response.rs rename to http/src/request/application/interaction/delete_original_response.rs diff --git a/http/src/request/application/get_original_response.rs b/http/src/request/application/interaction/get_original_response.rs similarity index 100% rename from http/src/request/application/get_original_response.rs rename to http/src/request/application/interaction/get_original_response.rs diff --git a/http/src/request/application/interaction_callback.rs b/http/src/request/application/interaction/interaction_callback.rs similarity index 100% rename from http/src/request/application/interaction_callback.rs rename to http/src/request/application/interaction/interaction_callback.rs diff --git a/http/src/request/application/interaction/mod.rs b/http/src/request/application/interaction/mod.rs new file mode 100644 index 00000000000..0e99aee4b3b --- /dev/null +++ b/http/src/request/application/interaction/mod.rs @@ -0,0 +1,15 @@ +pub mod create_followup_message; +pub mod update_followup_message; +pub mod update_original_response; + +mod delete_followup_message; +mod delete_original_response; +mod get_original_response; +mod interaction_callback; + +pub use self::{ + create_followup_message::CreateFollowupMessage, delete_followup_message::DeleteFollowupMessage, + delete_original_response::DeleteOriginalResponse, get_original_response::GetOriginalResponse, + interaction_callback::InteractionCallback, update_followup_message::UpdateFollowupMessage, + update_original_response::UpdateOriginalResponse, +}; diff --git a/http/src/request/application/update_followup_message.rs b/http/src/request/application/interaction/update_followup_message.rs similarity index 100% rename from http/src/request/application/update_followup_message.rs rename to http/src/request/application/interaction/update_followup_message.rs diff --git a/http/src/request/application/update_original_response.rs b/http/src/request/application/interaction/update_original_response.rs similarity index 100% rename from http/src/request/application/update_original_response.rs rename to http/src/request/application/interaction/update_original_response.rs diff --git a/http/src/request/application/mod.rs b/http/src/request/application/mod.rs index 68c6850fe08..d67ff61b574 100644 --- a/http/src/request/application/mod.rs +++ b/http/src/request/application/mod.rs @@ -1,60 +1,98 @@ -pub mod create_followup_message; - -mod create_global_command; -mod create_guild_command; -mod delete_followup_message; -mod delete_global_command; -mod delete_guild_command; -mod delete_original_response; -mod get_command_permissions; -mod get_global_commands; -mod get_guild_command_permissions; -mod get_guild_commands; -mod get_original_response; -mod interaction_callback; -mod set_command_permissions; -mod set_global_commands; -mod set_guild_commands; -mod update_command_permissions; -mod update_followup_message; -mod update_global_command; -mod update_guild_command; -mod update_original_response; - -pub use self::{ - create_followup_message::CreateFollowupMessage, - create_global_command::CreateGlobalCommand, - create_guild_command::CreateGuildCommand, - delete_followup_message::DeleteFollowupMessage, - delete_global_command::DeleteGlobalCommand, - delete_guild_command::DeleteGuildCommand, - delete_original_response::DeleteOriginalResponse, - get_command_permissions::GetCommandPermissions, - get_global_commands::GetGlobalCommands, - get_guild_command_permissions::GetGuildCommandPermissions, - get_guild_commands::GetGuildCommands, - get_original_response::GetOriginalResponse, - interaction_callback::InteractionCallback, - set_command_permissions::SetCommandPermissions, - set_global_commands::SetGlobalCommands, - set_guild_commands::SetGuildCommands, - update_command_permissions::UpdateCommandPermissions, - update_followup_message::{ - UpdateFollowupMessage, UpdateFollowupMessageError, UpdateFollowupMessageErrorType, - }, - update_global_command::UpdateGlobalCommand, - update_guild_command::UpdateGuildCommand, - update_original_response::{ - UpdateOriginalResponse, UpdateOriginalResponseError, UpdateOriginalResponseErrorType, - }, -}; +pub mod command; +pub mod interaction; + +/// Alias of [`interaction::CreateFollowupMessage`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type CreateFollowupMessage<'a> = interaction::CreateFollowupMessage<'a>; + +/// Alias of [`interaction::DeleteOriginalResponse`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type DeleteOriginalResponse<'a> = interaction::DeleteOriginalResponse<'a>; + +/// Alias of [`interaction::GetOriginalResponse`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type GetOriginalResponse<'a> = interaction::GetOriginalResponse<'a>; + +/// Alias of [`interaction::InteractionCallback`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type InteractionCallback<'a> = interaction::InteractionCallback<'a>; + +/// Alias of [`interaction::UpdateFollowupMessage`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type UpdateFollowupMessage<'a> = interaction::UpdateFollowupMessage<'a>; + +/// Alias of [`interaction::UpdateOriginalResponse`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type UpdateOriginalResponse<'a> = interaction::UpdateOriginalResponse<'a>; + +/// Alias of [`command::CreateGlobalCommand`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type CreateGlobalCommand<'a> = command::CreateGlobalCommand<'a>; + +/// Alias of [`command::CreateGuildCommand`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type CreateGuildCommand<'a> = command::CreateGuildCommand<'a>; + +/// Alias of [`command::DeleteGlobalCommand`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type DeleteGlobalCommand<'a> = command::DeleteGlobalCommand<'a>; + +/// Alias of [`command::DeleteGuildCommand`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type DeleteGuildCommand<'a> = command::DeleteGuildCommand<'a>; + +/// Alias of [`command::GetCommandPermissions`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type GetCommandPermissions<'a> = command::GetCommandPermissions<'a>; + +/// Alias of [`command::GetGlobalCommand`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type GetGlobalCommand<'a> = command::GetGlobalCommand<'a>; + +/// Alias of [`command::GetGlobalCommands`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type GetGlobalCommands<'a> = command::GetGlobalCommands<'a>; + +/// Alias of [`command::GetGuildCommand`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type GetGuildCommand<'a> = command::GetGuildCommand<'a>; + +/// Alias of [`command::GetGuildCommandPermissions`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type GetGuildCommandPermissions<'a> = command::GetGuildCommandPermissions<'a>; + +/// Alias of [`command::GetGuildCommands`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type GetGuildCommands<'a> = command::GetGuildCommands<'a>; + +/// Alias of [`command::SetCommandPermissions`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type SetCommandPermissions<'a> = command::SetCommandPermissions<'a>; + +/// Alias of [`command::SetGlobalCommands`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type SetGlobalCommands<'a> = command::SetGlobalCommands<'a>; + +/// Alias of [`command::SetGuildCommands`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type SetGuildCommands<'a> = command::SetGuildCommands<'a>; + +/// Alias of [`command::UpdateCommandPermissions`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type UpdateCommandPermissions<'a> = command::UpdateCommandPermissions<'a>; + +/// Alias of [`command::UpdateGlobalCommand`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type UpdateGlobalCommand<'a> = command::UpdateGlobalCommand<'a>; + +/// Alias of [`command::UpdateGuildCommand`]. +#[deprecated(note = "moved to `command` and `interaction` modules", since = "0.6.4")] +pub type UpdateGuildCommand<'a> = command::UpdateGuildCommand<'a>; -use serde::Serialize; use std::{ error::Error, fmt::{Display, Formatter, Result as FmtResult}, }; -use twilight_model::{application::command::CommandOption, id::ApplicationId}; /// The error created if the creation of interaction fails. #[derive(Debug)] @@ -146,57 +184,3 @@ impl Display for InteractionError { } impl Error for InteractionError {} - -/// Version of [`Command`] but with borrowed fields. -/// -/// [`Command`]: twilight_model::application::command::Command -#[derive(Serialize)] -struct CommandBorrowed<'a> { - #[serde(skip_serializing_if = "Option::is_none")] - pub application_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub default_permission: Option, - pub description: &'a str, - pub name: &'a str, - #[serde(default)] - pub options: Option<&'a [CommandOption]>, -} - -#[cfg(test)] -mod tests { - use super::CommandBorrowed; - use twilight_model::{ - application::command::{BaseCommandOptionData, Command, CommandOption}, - id::{ApplicationId, CommandId, GuildId}, - }; - - /// Test to convert a `Command` to a `CommandBorrowed`. - /// - /// Notably the point of this is to ensure that if a field is added to - /// `Command` or a type is changed then the destructure of it and creation - /// of `CommandBorrowed` will fail. - #[test] - fn test_command_borrowed_from_command() { - let command = Command { - application_id: Some(ApplicationId(1)), - default_permission: Some(true), - description: "command description".to_owned(), - guild_id: Some(GuildId(2)), - name: "command name".to_owned(), - id: Some(CommandId(3)), - options: Vec::from([CommandOption::Boolean(BaseCommandOptionData { - description: "command description".to_owned(), - name: "command name".to_owned(), - required: true, - })]), - }; - - let _ = CommandBorrowed { - application_id: command.application_id, - default_permission: command.default_permission, - description: &command.description, - name: &command.name, - options: Some(&command.options), - }; - } -} diff --git a/http/src/request/application/set_command_permissions.rs b/http/src/request/application/set_command_permissions.rs deleted file mode 100644 index 7ef53663501..00000000000 --- a/http/src/request/application/set_command_permissions.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::{ - client::Client, - error::Error, - request::{ - application::{InteractionError, InteractionErrorType}, - validate_inner, Request, RequestBuilder, - }, - response::ResponseFuture, - routing::Route, -}; -use serde::{Serialize, Serializer}; -use twilight_model::{ - application::command::permissions::CommandPermissions, - id::{ApplicationId, CommandId, GuildId}, -}; - -#[derive(Serialize)] -struct GuildCommandPermission<'a> { - id: &'a CommandId, - permissions: &'a CommandPermissions, -} - -struct PermissionListSerializer<'a> { - inner: &'a [(CommandId, CommandPermissions)], -} - -impl Serialize for PermissionListSerializer<'_> { - fn serialize(&self, serializer: S) -> Result { - serializer.collect_seq( - self.inner - .iter() - .map(|(id, permissions)| GuildCommandPermission { id, permissions }), - ) - } -} - -/// Update command permissions for all commands in a guild. -/// -/// This overwrites the command permissions so the full set of permissions -/// have to be sent every time. -#[derive(Debug)] -#[must_use = "requests must be configured and executed"] -pub struct SetCommandPermissions<'a> { - application_id: ApplicationId, - guild_id: GuildId, - http: &'a Client, - permissions: &'a [(CommandId, CommandPermissions)], -} - -impl<'a> SetCommandPermissions<'a> { - pub(crate) fn new( - http: &'a Client, - application_id: ApplicationId, - guild_id: GuildId, - permissions: &'a [(CommandId, CommandPermissions)], - ) -> Result { - let mut sorted_permissions = - [(CommandId(u64::MAX), 0); InteractionError::GUILD_COMMAND_LIMIT]; - - 'outer: for (permission_id, _) in permissions { - for (ref mut sorted_id, ref mut count) in &mut sorted_permissions { - if *sorted_id == *permission_id { - *count += 1; - - if !validate_inner::guild_command_permissions(*count) { - return Err(InteractionError { - kind: InteractionErrorType::TooManyCommandPermissions, - }); - } - - continue 'outer; - } else if sorted_id.0 == u64::MAX { - *count += 1; - *sorted_id = *permission_id; - - continue 'outer; - } - } - - // We've run out of space in the sorted permissions, which means the - // user provided too many commands. - return Err(InteractionError { - kind: InteractionErrorType::TooManyCommands, - }); - } - - Ok(Self { - application_id, - guild_id, - http, - permissions, - }) - } - - fn request(&self) -> Result { - Request::builder(&Route::SetCommandPermissions { - application_id: self.application_id.0, - guild_id: self.guild_id.0, - }) - .json(&PermissionListSerializer { - inner: self.permissions, - }) - .map(RequestBuilder::build) - } - - /// Execute the request, returning a future resolving to a [`Response`]. - /// - /// [`Response`]: crate::response::Response - pub fn exec(self) -> ResponseFuture { - match self.request() { - Ok(request) => self.http.request(request), - Err(source) => ResponseFuture::error(source), - } - } -} - -#[cfg(test)] -mod tests { - use super::{ - super::{InteractionError, InteractionErrorType}, - SetCommandPermissions, - }; - use crate::Client; - use std::iter; - use twilight_model::{ - application::command::permissions::{CommandPermissions, CommandPermissionsType}, - id::{ApplicationId, CommandId, GuildId, RoleId}, - }; - - const APPLICATION_ID: ApplicationId = ApplicationId(1); - const GUILD_ID: GuildId = GuildId(2); - - fn command_permissions(id: CommandId) -> impl Iterator { - iter::repeat(( - id, - CommandPermissions { - id: CommandPermissionsType::Role(RoleId(4)), - permission: true, - }, - )) - } - - #[test] - fn test_correct_validation() { - let http = Client::new("token".to_owned()); - let command_permissions = command_permissions(CommandId(1)) - .take(4) - .collect::>(); - - let request = - SetCommandPermissions::new(&http, APPLICATION_ID, GUILD_ID, &command_permissions); - - assert!(request.is_ok()); - } - - #[test] - fn test_incorrect_validation() { - let http = Client::new("token".to_owned()); - let command_permissions = command_permissions(CommandId(2)) - .take(InteractionError::GUILD_COMMAND_PERMISSION_LIMIT + 1) - .collect::>(); - - let request = - SetCommandPermissions::new(&http, APPLICATION_ID, GUILD_ID, &command_permissions); - assert!(matches!( - request.unwrap_err().kind(), - InteractionErrorType::TooManyCommandPermissions - )); - } - - #[test] - fn test_limits() { - const SIZE: usize = InteractionError::GUILD_COMMAND_LIMIT; - - let http = Client::new("token".to_owned()); - let command_permissions = (1..=SIZE) - .flat_map(|id| { - command_permissions(CommandId(id as u64)) - .take(InteractionError::GUILD_COMMAND_PERMISSION_LIMIT) - }) - .collect::>(); - - assert!( - SetCommandPermissions::new(&http, APPLICATION_ID, GUILD_ID, &command_permissions) - .is_ok() - ); - } - - #[test] - fn test_command_count_over_limit() { - const SIZE: usize = 101; - - let http = Client::new("token".to_owned()); - let command_permissions = (1..=SIZE) - .flat_map(|id| command_permissions(CommandId(id as u64)).take(3)) - .collect::>(); - - let request = - SetCommandPermissions::new(&http, APPLICATION_ID, GUILD_ID, &command_permissions); - assert!(matches!( - request.unwrap_err().kind(), - InteractionErrorType::TooManyCommands - )); - } - - #[test] - fn test_no_permissions() { - let http = Client::new("token".to_owned()); - - assert!(SetCommandPermissions::new(&http, APPLICATION_ID, GUILD_ID, &[]).is_ok()); - } -} diff --git a/http/src/request/validate.rs b/http/src/request/validate.rs index 32f1d3faf1a..fd1fa165d90 100644 --- a/http/src/request/validate.rs +++ b/http/src/request/validate.rs @@ -1125,8 +1125,8 @@ pub fn command_name(value: impl AsRef) -> bool { fn _command_name(value: &str) -> bool { let len = value.chars().count(); - // https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoption - (3..=32).contains(&len) + // https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure + (1..=32).contains(&len) } pub fn command_description(value: impl AsRef) -> bool { @@ -1136,12 +1136,12 @@ pub fn command_description(value: impl AsRef) -> bool { fn _command_description(value: &str) -> bool { let len = value.chars().count(); - // https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoption + // https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure (1..=100).contains(&len) } pub const fn command_permissions(len: usize) -> bool { - // https://discord.com/developers/docs/interactions/slash-commands#edit-application-command-permissions + // https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions len <= 10 } @@ -1150,7 +1150,7 @@ pub const fn command_permissions(len: usize) -> bool { /// The maximum number of commands allowed in a guild is defined by /// [`InteractionError::GUILD_COMMAND_PERMISSION_LIMIT`]. pub const fn guild_command_permissions(count: usize) -> bool { - // https://discord.com/developers/docs/interactions/slash-commands#a-quick-note-on-limits + // https://discord.com/developers/docs/interactions/application-commands#registering-a-command count <= InteractionError::GUILD_COMMAND_PERMISSION_LIMIT } diff --git a/http/src/response/status_code.rs b/http/src/response/status_code.rs index a19337f5f21..372fd86e672 100644 --- a/http/src/response/status_code.rs +++ b/http/src/response/status_code.rs @@ -1,6 +1,12 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; /// Status code of a response. +/// +/// # Comparing the status code +/// +/// Status codes can easily be compared with status code integers due to +/// implementing `PartialEq`. This is equivalent to checking against the +/// value returned by [`StatusCode::raw`]. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct StatusCode(u16); @@ -72,6 +78,18 @@ impl Display for StatusCode { } } +impl PartialEq for StatusCode { + fn eq(&self, other: &u16) -> bool { + self.raw() == *other + } +} + +impl PartialEq for u16 { + fn eq(&self, other: &StatusCode) -> bool { + *self == other.raw() + } +} + #[cfg(test)] mod tests { use super::StatusCode; @@ -95,6 +113,12 @@ mod tests { Sync ); + #[test] + fn test_eq_with_integer() { + assert_eq!(200_u16, StatusCode::new(200)); + assert_eq!(StatusCode::new(404), 404_u16); + } + #[test] fn test_ranges() { assert!(StatusCode::new(100).is_informational()); diff --git a/http/src/routing/route.rs b/http/src/routing/route.rs index 60607d6ee8f..fa0b45cdb97 100644 --- a/http/src/routing/route.rs +++ b/http/src/routing/route.rs @@ -364,6 +364,13 @@ pub enum Route<'a> { /// Route information to get gateway information tailored to the current /// user. GetGatewayBot, + /// Route information to get a global command for an application. + GetGlobalCommand { + /// ID of the owner application. + application_id: u64, + /// ID of the command. + command_id: u64, + }, GetGlobalCommands { /// The ID of the owner application. application_id: u64, @@ -376,6 +383,15 @@ pub enum Route<'a> { /// guild. with_counts: bool, }, + /// Route information to get a guild command. + GetGuildCommand { + /// ID of the owner application. + application_id: u64, + /// ID of the command. + command_id: u64, + /// ID of the guild. + guild_id: u64, + }, /// Route information to get permissions of all guild commands. GetGuildCommandPermissions { /// The ID of the application. @@ -914,8 +930,10 @@ impl<'a> Route<'a> { | Self::GetEmoji { .. } | Self::GetEmojis { .. } | Self::GetGateway + | Self::GetGlobalCommand { .. } | Self::GetGlobalCommands { .. } | Self::GetGuild { .. } + | Self::GetGuildCommand { .. } | Self::GetGuildCommandPermissions { .. } | Self::GetGuildCommands { .. } | Self::GetGuildIntegrations { .. } @@ -1062,6 +1080,7 @@ impl<'a> Route<'a> { } Self::CreateGuildCommand { application_id, .. } | Self::DeleteGuildCommand { application_id, .. } + | Self::GetGuildCommand { application_id, .. } | Self::GetGuildCommandPermissions { application_id, .. } | Self::GetGuildCommands { application_id, .. } | Self::SetCommandPermissions { application_id, .. } @@ -1105,6 +1124,7 @@ impl<'a> Route<'a> { Self::DeleteChannel { channel_id } => Path::ChannelsId(*channel_id), Self::DeleteEmoji { guild_id, .. } => Path::GuildsIdEmojisId(*guild_id), Self::DeleteGlobalCommand { application_id, .. } + | Self::GetGlobalCommand { application_id, .. } | Self::UpdateGlobalCommand { application_id, .. } => { Path::ApplicationCommandId(*application_id) } diff --git a/http/src/routing/route_display.rs b/http/src/routing/route_display.rs index 0658ccf5dfd..528f88e6135 100644 --- a/http/src/routing/route_display.rs +++ b/http/src/routing/route_display.rs @@ -289,6 +289,10 @@ impl Display for RouteDisplay<'_> { application_id, command_id, } + | Route::GetGlobalCommand { + application_id, + command_id, + } | Route::UpdateGlobalCommand { application_id, command_id, @@ -309,6 +313,11 @@ impl Display for RouteDisplay<'_> { command_id, guild_id, } + | Route::GetGuildCommand { + application_id, + command_id, + guild_id, + } | Route::UpdateGuildCommand { application_id, command_id, diff --git a/model/CHANGELOG.md b/model/CHANGELOG.md index 71b080986c7..bb4810150ea 100644 --- a/model/CHANGELOG.md +++ b/model/CHANGELOG.md @@ -2,6 +2,25 @@ Changelog for `twilight-model`. +## [0.6.2] - 2021-08-30 + +### Additions + +Support message components, including action rows, buttons, and select menus +([#1020], [#1043], [#1044], [#1090], aggregate [#1121] - [@AEnterprise], +[@AsianIntel], [@zeylahellyer], [@7596ff]). + +### Enhancements + +Fix a remaining intradoc link ([#1128] - [@zeylahellyer]). + +[#1128]: https://github.com/twilight-rs/twilight/pull/1128 +[#1121]: https://github.com/twilight-rs/twilight/pull/1121 +[#1090]: https://github.com/twilight-rs/twilight/pull/1090 +[#1044]: https://github.com/twilight-rs/twilight/pull/1044 +[#1043]: https://github.com/twilight-rs/twilight/pull/1043 +[#1020]: https://github.com/twilight-rs/twilight/pull/1020 + ## [0.6.1] - 2021-08-18 ### Fixes @@ -750,6 +769,7 @@ Initial release. [@7596ff]: https://github.com/7596ff [@A5rocks]: https://github.com/A5rocks +[@AEnterprise]: https://github.com/AEnterprise [@AsianIntel]: https://github.com/AsianIntel [@BlackHoleFox]: https://github.com/BlackHoleFox [@chamburr]: https://github.com/chamburr @@ -790,6 +810,7 @@ Initial release. [0.2.0-beta.1:app integrations]: https://github.com/discord/discord-api-docs/commit/a926694e2f8605848bda6b57d21c8817559e5cec +[0.6.2]: https://github.com/twilight-rs/twilight/releases/tag/model-0.6.2 [0.6.1]: https://github.com/twilight-rs/twilight/releases/tag/model-0.6.1 [0.5.4]: https://github.com/twilight-rs/twilight/releases/tag/model-0.5.4 [0.5.3]: https://github.com/twilight-rs/twilight/releases/tag/model-0.5.3 diff --git a/model/Cargo.toml b/model/Cargo.toml index 68f832bcdf6..419270a0330 100644 --- a/model/Cargo.toml +++ b/model/Cargo.toml @@ -12,7 +12,7 @@ name = "twilight-model" publish = false readme = "README.md" repository = "https://github.com/twilight-rs/twilight.git" -version = "0.6.1" +version = "0.6.2" [dependencies] bitflags = { default-features = false, version = "1" } diff --git a/model/src/application/callback/mod.rs b/model/src/application/callback/mod.rs index 7429e559db8..e6cfd7173db 100644 --- a/model/src/application/callback/mod.rs +++ b/model/src/application/callback/mod.rs @@ -16,7 +16,7 @@ use std::fmt::{Formatter, Result as FmtResult}; /// /// Refer to [the discord docs] for more information. /// -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#interaction-response +/// [the discord docs]: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-structure #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum InteractionResponse { /// Used when responding to an interaction of type Ping. diff --git a/model/src/application/command/command_type.rs b/model/src/application/command/command_type.rs new file mode 100644 index 00000000000..55928ec0a20 --- /dev/null +++ b/model/src/application/command/command_type.rs @@ -0,0 +1,68 @@ +use serde_repr::{Deserialize_repr, Serialize_repr}; + +#[derive( + Clone, Copy, Debug, Deserialize_repr, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize_repr, +)] +#[repr(u8)] +pub enum CommandType { + /// Slash command. + /// + /// Text-based command that appears when a user types `/`. + ChatInput = 1, + /// UI-based command. + /// + /// Appears when a user right clicks or taps om a user. + User = 2, + /// UI-based command. + /// + /// Appears when a user right clicks or taps on a message. + Message = 3, +} + +impl CommandType { + pub const fn kind(self) -> &'static str { + match self { + Self::ChatInput => "ChatInput", + Self::User => "User", + Self::Message => "Message", + } + } +} + +#[cfg(test)] +mod tests { + use super::CommandType; + use serde::{Deserialize, Serialize}; + use serde_test::Token; + use static_assertions::assert_impl_all; + use std::{fmt::Debug, hash::Hash}; + + assert_impl_all!( + CommandType: Clone, + Copy, + Debug, + Deserialize<'static>, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + Serialize, + Send, + Sync + ); + + #[test] + fn test_variants() { + serde_test::assert_tokens(&CommandType::ChatInput, &[Token::U8(1)]); + serde_test::assert_tokens(&CommandType::User, &[Token::U8(2)]); + serde_test::assert_tokens(&CommandType::Message, &[Token::U8(3)]); + } + + #[test] + fn test_kinds() { + assert_eq!("ChatInput", CommandType::ChatInput.kind()); + assert_eq!("User", CommandType::User.kind()); + assert_eq!("Message", CommandType::Message.kind()); + } +} diff --git a/model/src/application/command/mod.rs b/model/src/application/command/mod.rs index e9e56454ee4..4f7ef57b6aa 100644 --- a/model/src/application/command/mod.rs +++ b/model/src/application/command/mod.rs @@ -2,11 +2,15 @@ pub mod permissions; +mod command_type; mod option; -pub use self::option::{ - BaseCommandOptionData, ChoiceCommandOptionData, CommandOption, CommandOptionChoice, - CommandOptionType, OptionsCommandOptionData, +pub use self::{ + command_type::CommandType, + option::{ + BaseCommandOptionData, ChoiceCommandOptionData, CommandOption, CommandOptionChoice, + CommandOptionType, OptionsCommandOptionData, + }, }; use crate::id::{ApplicationId, CommandId, GuildId}; @@ -18,7 +22,7 @@ use serde::{Deserialize, Serialize}; /// Command names must be lower case, matching the Regex `^[\w-]{1,32}$`. Refer /// to [the discord docs] for more information. /// -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#applicationcommand +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#applicationcommand #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Command { #[serde(skip_serializing_if = "Option::is_none")] @@ -29,9 +33,17 @@ pub struct Command { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub default_permission: Option, + /// Description of the command. + /// + /// For [`User`] and [`Message`] commands, this will be an empty string. + /// + /// [`User`]: CommandType::User + /// [`Message`]: CommandType::Message pub description: String, #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, + #[serde(rename = "type")] + pub kind: CommandType, #[serde(default)] pub options: Vec, } diff --git a/model/src/application/command/option.rs b/model/src/application/command/option.rs index 064c6d6f58b..82a2e646a79 100644 --- a/model/src/application/command/option.rs +++ b/model/src/application/command/option.rs @@ -228,9 +228,7 @@ impl<'de> Visitor<'de> for OptionVisitor { Ok(match kind { CommandOptionType::SubCommand => { - let options = options - .flatten() - .ok_or_else(|| DeError::missing_field("options"))?; + let options = options.flatten().unwrap_or_default(); CommandOption::SubCommand(OptionsCommandOptionData { description, @@ -240,9 +238,7 @@ impl<'de> Visitor<'de> for OptionVisitor { }) } CommandOptionType::SubCommandGroup => { - let options = options - .flatten() - .ok_or_else(|| DeError::missing_field("options"))?; + let options = options.flatten().unwrap_or_default(); CommandOption::SubCommandGroup(OptionsCommandOptionData { description, @@ -361,7 +357,7 @@ pub struct ChoiceCommandOptionData { /// /// Refer to [the discord docs] for more information. /// -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#applicationcommandoptionchoice #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[serde(untagged)] pub enum CommandOptionChoice { @@ -405,18 +401,52 @@ impl CommandOptionType { #[cfg(test)] mod tests { use super::{ - super::Command, BaseCommandOptionData, ChoiceCommandOptionData, CommandOption, - CommandOptionChoice, OptionsCommandOptionData, + super::{Command, CommandType}, + BaseCommandOptionData, ChoiceCommandOptionData, CommandOption, CommandOptionChoice, + OptionsCommandOptionData, }; use crate::id::{ApplicationId, CommandId, GuildId}; use serde_test::Token; + /// Test that when a subcommand or subcommand group's `options` field is + /// missing during deserialization that the field is defaulted instead of + /// returning a missing field error. + #[test] + fn test_issue_1150() { + let value = CommandOption::SubCommand(OptionsCommandOptionData { + description: "ponyville".to_owned(), + name: "equestria".to_owned(), + options: Vec::new(), + required: false, + }); + + serde_test::assert_de_tokens( + &value, + &[ + Token::Struct { + name: "CommandOptionEnvelope", + len: 4, + }, + Token::Str("description"), + Token::Str("ponyville"), + Token::Str("name"), + Token::Str("equestria"), + Token::Str("options"), + Token::None, + Token::Str("type"), + Token::U8(1), + Token::StructEnd, + ], + ); + } + #[test] #[allow(clippy::too_many_lines)] fn test_command_option_full() { let value = Command { application_id: Some(ApplicationId(100)), guild_id: Some(GuildId(300)), + kind: CommandType::ChatInput, name: "test command".into(), default_permission: Some(true), description: "this command is a test".into(), @@ -489,7 +519,7 @@ mod tests { &[ Token::Struct { name: "Command", - len: 7, + len: 8, }, Token::Str("application_id"), Token::Some, @@ -512,6 +542,8 @@ mod tests { Token::Some, Token::NewtypeStruct { name: "CommandId" }, Token::Str("200"), + Token::Str("type"), + Token::U8(1), Token::Str("options"), Token::Seq { len: Some(1) }, Token::Struct { diff --git a/model/src/application/interaction/application_command/data/mod.rs b/model/src/application/interaction/application_command/data/mod.rs index 8aaad21d0bf..172387a0c89 100644 --- a/model/src/application/interaction/application_command/data/mod.rs +++ b/model/src/application/interaction/application_command/data/mod.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; /// Refer to [the discord docs] for more information. /// /// [`ApplicationCommand`]: crate::application::interaction::Interaction::ApplicationCommand -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondata +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#interaction-applicationcommandinteractiondata #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub struct CommandData { /// ID of the command. @@ -31,7 +31,7 @@ pub struct CommandData { /// /// Refer to [the discord docs] for more information. /// -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondataoption +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#interaction-applicationcommandinteractiondataoption #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(untagged)] pub enum CommandDataOption { diff --git a/model/src/application/interaction/application_command/data/resolved.rs b/model/src/application/interaction/application_command/data/resolved.rs index 33082601add..8a8d60918ba 100644 --- a/model/src/application/interaction/application_command/data/resolved.rs +++ b/model/src/application/interaction/application_command/data/resolved.rs @@ -1,7 +1,7 @@ use crate::{ - channel::ChannelType, + channel::{ChannelType, Message}, guild::{Permissions, Role}, - id::{ChannelId, RoleId, UserId}, + id::{ChannelId, MessageId, RoleId, UserId}, user::User, }; use serde::{ @@ -18,6 +18,7 @@ use std::{ pub struct CommandInteractionDataResolved { pub channels: Vec, pub members: Vec, + pub messages: Vec, pub roles: Vec, pub users: Vec, } @@ -33,6 +34,7 @@ impl Serialize for CommandInteractionDataResolved { let len = vec![ self.channels.is_empty(), self.members.is_empty(), + self.messages.is_empty(), self.roles.is_empty(), self.users.is_empty(), ] @@ -64,6 +66,17 @@ impl Serialize for CommandInteractionDataResolved { state.serialize_field("members", &map)?; } + if !self.messages.is_empty() { + let map: HashMap = self + .messages + .iter() + .map(|m| m.id) + .zip(self.messages.iter()) + .collect(); + + state.serialize_field("messages", &map)?; + } + if !self.roles.is_empty() { let map: HashMap = self .roles @@ -95,6 +108,7 @@ impl Serialize for CommandInteractionDataResolved { enum ResolvedField { Channels, Members, + Messages, Roles, Users, } @@ -111,6 +125,7 @@ impl<'de> Visitor<'de> for ResolvedVisitor { fn visit_map>(self, mut map: V) -> Result { let mut channels: Option> = None; let mut members: Option> = None; + let mut messages: Option> = None; let mut roles: Option> = None; let mut users: Option> = None; @@ -154,6 +169,15 @@ impl<'de> Visitor<'de> for ResolvedVisitor { .collect(), ); } + ResolvedField::Messages => { + if messages.is_some() { + return Err(DeError::duplicate_field("messages")); + } + + let mapped_messages: HashMap = map.next_value()?; + + messages = Some(mapped_messages.into_iter().map(|(_, v)| v).collect()); + } ResolvedField::Roles => { if roles.is_some() { return Err(DeError::duplicate_field("roles")); @@ -178,6 +202,7 @@ impl<'de> Visitor<'de> for ResolvedVisitor { Ok(CommandInteractionDataResolved { channels: channels.unwrap_or_default(), members: members.unwrap_or_default(), + messages: messages.unwrap_or_default(), roles: roles.unwrap_or_default(), users: users.unwrap_or_default(), }) @@ -224,9 +249,15 @@ struct InteractionMemberEnvelope { mod tests { use super::{CommandInteractionDataResolved, InteractionChannel, InteractionMember}; use crate::{ - channel::ChannelType, - guild::{Permissions, Role}, - id::{ChannelId, RoleId, UserId}, + channel::{ + message::{ + sticker::{MessageSticker, StickerFormatType, StickerId}, + MessageFlags, MessageType, + }, + ChannelType, Message, + }, + guild::{PartialMember, Permissions, Role}, + id::{ChannelId, GuildId, MessageId, RoleId, UserId}, user::{PremiumType, User, UserFlags}, }; use serde_test::Token; @@ -235,21 +266,80 @@ mod tests { #[allow(clippy::too_many_lines)] fn test_data_resolved() { let value = CommandInteractionDataResolved { - channels: vec![InteractionChannel { + channels: Vec::from([InteractionChannel { id: ChannelId(100), kind: ChannelType::GuildText, name: "channel name".into(), permissions: Permissions::empty(), - }], - members: vec![InteractionMember { + }]), + members: Vec::from([InteractionMember { hoisted_role: None, id: UserId(300), joined_at: Some("joined at".into()), nick: None, premium_since: None, roles: Vec::new(), - }], - roles: vec![Role { + }]), + messages: Vec::from([Message { + activity: None, + application: None, + application_id: None, + attachments: Vec::new(), + author: User { + accent_color: None, + avatar: Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned()), + banner: None, + bot: false, + discriminator: "0001".to_owned(), + email: None, + flags: None, + id: UserId(3), + locale: None, + mfa_enabled: None, + name: "test".to_owned(), + premium_type: None, + public_flags: None, + system: None, + verified: None, + }, + channel_id: ChannelId(2), + components: Vec::new(), + content: "ping".to_owned(), + edited_timestamp: None, + embeds: Vec::new(), + flags: Some(MessageFlags::empty()), + guild_id: Some(GuildId(1)), + id: MessageId(4), + interaction: None, + kind: MessageType::Regular, + member: Some(PartialMember { + deaf: false, + joined_at: Some("2020-01-01T00:00:00.000000+00:00".to_owned()), + mute: false, + nick: Some("member nick".to_owned()), + permissions: None, + premium_since: None, + roles: Vec::new(), + user: None, + }), + mention_channels: Vec::new(), + mention_everyone: false, + mention_roles: Vec::new(), + mentions: Vec::new(), + pinned: false, + reactions: Vec::new(), + reference: None, + sticker_items: vec![MessageSticker { + format_type: StickerFormatType::Png, + id: StickerId(1), + name: "sticker name".to_owned(), + }], + referenced_message: None, + timestamp: "2020-02-02T02:02:02.020000+00:00".to_owned(), + tts: false, + webhook_id: None, + }]), + roles: Vec::from([Role { color: 0, hoist: true, id: RoleId(400), @@ -259,8 +349,8 @@ mod tests { permissions: Permissions::ADMINISTRATOR, position: 12, tags: None, - }], - users: vec![User { + }]), + users: Vec::from([User { accent_color: None, avatar: Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned()), banner: None, @@ -276,7 +366,7 @@ mod tests { public_flags: Some(UserFlags::EARLY_SUPPORTER | UserFlags::VERIFIED_BOT_DEVELOPER), system: None, verified: Some(true), - }], + }]), }; serde_test::assert_tokens( @@ -284,7 +374,7 @@ mod tests { &[ Token::Struct { name: "CommandInteractionDataResolved", - len: 4, + len: 5, }, Token::Str("channels"), Token::Map { len: Some(1) }, @@ -318,6 +408,116 @@ mod tests { Token::Str("joined at"), Token::StructEnd, Token::MapEnd, + Token::Str("messages"), + Token::Map { len: Some(1) }, + Token::NewtypeStruct { name: "MessageId" }, + Token::Str("4"), + Token::Struct { + name: "Message", + len: 18, + }, + Token::Str("attachments"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, + Token::Str("author"), + Token::Struct { + name: "User", + len: 7, + }, + Token::Str("accent_color"), + Token::None, + Token::Str("avatar"), + Token::Some, + Token::Str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + Token::Str("banner"), + Token::None, + Token::Str("bot"), + Token::Bool(false), + Token::Str("discriminator"), + Token::Str("0001"), + Token::Str("id"), + Token::NewtypeStruct { name: "UserId" }, + Token::Str("3"), + Token::Str("username"), + Token::Str("test"), + Token::StructEnd, + Token::Str("channel_id"), + Token::NewtypeStruct { name: "ChannelId" }, + Token::Str("2"), + Token::Str("content"), + Token::Str("ping"), + Token::Str("edited_timestamp"), + Token::None, + Token::Str("embeds"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, + Token::Str("flags"), + Token::Some, + Token::U64(0), + Token::Str("guild_id"), + Token::Some, + Token::NewtypeStruct { name: "GuildId" }, + Token::Str("1"), + Token::Str("id"), + Token::NewtypeStruct { name: "MessageId" }, + Token::Str("4"), + Token::Str("type"), + Token::U8(0), + Token::Str("member"), + Token::Some, + Token::Struct { + name: "PartialMember", + len: 7, + }, + Token::Str("deaf"), + Token::Bool(false), + Token::Str("joined_at"), + Token::Some, + Token::Str("2020-01-01T00:00:00.000000+00:00"), + Token::Str("mute"), + Token::Bool(false), + Token::Str("nick"), + Token::Some, + Token::Str("member nick"), + Token::Str("permissions"), + Token::None, + Token::Str("roles"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, + Token::Str("user"), + Token::None, + Token::StructEnd, + Token::Str("mention_everyone"), + Token::Bool(false), + Token::Str("mention_roles"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, + Token::Str("mentions"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, + Token::Str("pinned"), + Token::Bool(false), + Token::Str("sticker_items"), + Token::Seq { len: Some(1) }, + Token::Struct { + name: "MessageSticker", + len: 3, + }, + Token::Str("format_type"), + Token::U8(1), + Token::Str("id"), + Token::NewtypeStruct { name: "StickerId" }, + Token::Str("1"), + Token::Str("name"), + Token::Str("sticker name"), + Token::StructEnd, + Token::SeqEnd, + Token::Str("timestamp"), + Token::Str("2020-02-02T02:02:02.020000+00:00"), + Token::Str("tts"), + Token::Bool(false), + Token::StructEnd, + Token::MapEnd, Token::Str("roles"), Token::Map { len: Some(1) }, Token::NewtypeStruct { name: "RoleId" }, diff --git a/model/src/application/interaction/interaction_type.rs b/model/src/application/interaction/interaction_type.rs index 20c66b83111..599980286bb 100644 --- a/model/src/application/interaction/interaction_type.rs +++ b/model/src/application/interaction/interaction_type.rs @@ -7,7 +7,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; /// /// Refer to [the discord docs] for more information. /// -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#interaction-interactiontype +/// [the discord docs]: https://discord.com/developers/docs/interactions/application-commands#interaction-interactiontype #[derive( Clone, Copy, Debug, Deserialize_repr, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize_repr, )] diff --git a/model/src/application/interaction/mod.rs b/model/src/application/interaction/mod.rs index de8c8b3fac4..13ee3a1d6bd 100644 --- a/model/src/application/interaction/mod.rs +++ b/model/src/application/interaction/mod.rs @@ -29,7 +29,7 @@ use std::fmt::{Formatter, Result as FmtResult}; /// Each variant corresponds to `InteractionType` in the discord docs. Refer to /// [the discord docs] for more information. /// -/// [the discord docs]: https://discord.com/developers/docs/interactions/slash-commands#interaction +/// [the discord docs]: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-structure #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(untagged)] #[non_exhaustive] @@ -305,6 +305,7 @@ mod test { resolved: Some(CommandInteractionDataResolved { channels: Vec::new(), members: Vec::new(), + messages: Vec::new(), roles: Vec::new(), users: vec![User { accent_color: None, diff --git a/model/src/channel/message/kind.rs b/model/src/channel/message/kind.rs index cd067e9559b..cebe8fe06fd 100644 --- a/model/src/channel/message/kind.rs +++ b/model/src/channel/message/kind.rs @@ -28,6 +28,7 @@ pub enum MessageType { Reply = 19, ApplicationCommand = 20, GuildInviteReminder = 22, + ContextMenuCommand = 23, } impl TryFrom for MessageType { @@ -55,6 +56,7 @@ impl TryFrom for MessageType { 19 => MessageType::Reply, 20 => MessageType::ApplicationCommand, 22 => MessageType::GuildInviteReminder, + 23 => MessageType::ContextMenuCommand, _ => return Err(ConversionError::MessageType(value)), }; @@ -96,6 +98,7 @@ mod tests { serde_test::assert_tokens(&MessageType::Reply, &[Token::U8(19)]); serde_test::assert_tokens(&MessageType::ApplicationCommand, &[Token::U8(20)]); serde_test::assert_tokens(&MessageType::GuildInviteReminder, &[Token::U8(22)]); + serde_test::assert_tokens(&MessageType::ContextMenuCommand, &[Token::U8(23)]); } #[test] @@ -168,6 +171,10 @@ mod tests { MessageType::try_from(22).unwrap(), MessageType::GuildInviteReminder ); + assert_eq!( + MessageType::try_from(23).unwrap(), + MessageType::ContextMenuCommand + ); assert_eq!( MessageType::try_from(250).unwrap_err(), ConversionError::MessageType(250) diff --git a/model/src/channel/message/mod.rs b/model/src/channel/message/mod.rs index 5052b7dba1d..52c7d7c566c 100644 --- a/model/src/channel/message/mod.rs +++ b/model/src/channel/message/mod.rs @@ -72,7 +72,7 @@ pub struct Message { pub reference: Option, /// The message associated with the [reference]. /// - /// [reference]: #structfield.reference + /// [reference]: Self::reference #[serde(skip_serializing_if = "Option::is_none")] pub referenced_message: Option>, /// Stickers within the message. diff --git a/util/Cargo.toml b/util/Cargo.toml index f883d375188..ca95ab69046 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -20,7 +20,7 @@ twilight-model = { default-features = false, optional = true, path = "../model" [dev-dependencies] chrono = { default-features = false, features = ["std"], version = "0.4" } static_assertions = { default-features = false, version = "1" } -time = { default-features = false, version = "0.2" } +time = { default-features = false, features = ["formatting"], version = "0.3" } [features] default = [] diff --git a/util/src/snowflake.rs b/util/src/snowflake.rs index 34836ffee5b..c764dffe33e 100644 --- a/util/src/snowflake.rs +++ b/util/src/snowflake.rs @@ -19,7 +19,7 @@ pub trait Snowflake { /// /// See when a user was created using [`chrono`](https://docs.rs/chrono): /// - /// ```rust + /// ``` /// use chrono::{Utc, TimeZone}; /// use twilight_util::snowflake::Snowflake; /// use twilight_model::id::UserId; @@ -34,17 +34,22 @@ pub trait Snowflake { /// /// See when a user was created using [`time`](https://docs.rs/time): /// - /// ```rust - /// use time::{Duration, Format, OffsetDateTime}; + /// ``` + /// use time::{Duration, format_description::well_known::Rfc3339, OffsetDateTime}; /// use twilight_util::snowflake::Snowflake; /// use twilight_model::id::UserId; /// + /// # fn main() -> Result<(), Box> { /// let id = UserId(105484726235607040); + /// // Convert milliseconds to seconds or nanoseconds. /// let dur = Duration::milliseconds(id.timestamp()); - /// // Or use seconds, at the cost of lost precision. - /// let ts = OffsetDateTime::from_unix_timestamp_nanos(dur.whole_nanoseconds()); /// - /// assert_eq!("2015-10-19T01:58:38+00:00", ts.format(Format::Rfc3339)); + /// let ts = OffsetDateTime::from_unix_timestamp(dur.whole_seconds())?; + /// let ts_milli = OffsetDateTime::from_unix_timestamp_nanos(dur.whole_nanoseconds())?; + /// + /// assert_eq!("2015-10-19T01:58:38Z", ts.format(&Rfc3339)?); + /// assert_eq!("2015-10-19T01:58:38.546Z", ts_milli.format(&Rfc3339)?); + /// # Ok(()) } /// ``` #[allow(clippy::cast_possible_wrap)] fn timestamp(&self) -> i64 {