From 4fae2896cc021372a3b2dfc6aab9503cc5b7eac0 Mon Sep 17 00:00:00 2001 From: Erk Date: Thu, 24 Oct 2024 15:00:06 +0200 Subject: [PATCH] feat(model): Implement user applications (#2323) --- .../src/event/interaction.rs | 11 +++- twilight-cache-inmemory/src/event/message.rs | 6 +- twilight-cache-inmemory/src/model/message.rs | 3 + twilight-cache-inmemory/src/test.rs | 7 ++- .../src/request/application/command/mod.rs | 3 + .../src/request/update_user_application.rs | 26 +++++++- twilight-model/benches/deserialization.rs | 6 ++ twilight-model/src/application/command/mod.rs | 12 +++- .../application/interaction/context_type.rs | 49 +++++++++++++++ .../src/application/interaction/metadata.rs | 36 +++++++++++ .../src/application/interaction/mod.rs | 54 +++++++++++++++-- .../src/application/interaction/resolved.rs | 7 ++- twilight-model/src/channel/message/mod.rs | 12 +++- twilight-model/src/guild/permissions.rs | 5 +- twilight-model/src/oauth/application.rs | 55 +++++++++++++---- .../src/oauth/application_integration_type.rs | 59 +++++++++++++++++++ .../current_authorization_information.rs | 21 +++---- twilight-model/src/oauth/mod.rs | 10 +++- twilight-standby/src/lib.rs | 9 ++- twilight-util/src/builder/command.rs | 10 +++- twilight-validate/src/command.rs | 6 ++ 21 files changed, 357 insertions(+), 50 deletions(-) create mode 100644 twilight-model/src/application/interaction/context_type.rs create mode 100644 twilight-model/src/application/interaction/metadata.rs create mode 100644 twilight-model/src/oauth/application_integration_type.rs diff --git a/twilight-cache-inmemory/src/event/interaction.rs b/twilight-cache-inmemory/src/event/interaction.rs index 2e7dfbd3844..2f224ec9c1c 100644 --- a/twilight-cache-inmemory/src/event/interaction.rs +++ b/twilight-cache-inmemory/src/event/interaction.rs @@ -79,6 +79,7 @@ mod tests { gateway::payload::incoming::InteractionCreate, guild::{MemberFlags, PartialMember, Permissions, Role, RoleFlags}, id::Id, + oauth::ApplicationIntegrationMap, user::User, util::{image_hash::ImageHashParseError, ImageHash, Timestamp}, }; @@ -97,6 +98,10 @@ mod tests { cache.update(&InteractionCreate(Interaction { app_permissions: Some(Permissions::SEND_MESSAGES), application_id: Id::new(1), + authorizing_integration_owners: ApplicationIntegrationMap { + guild: None, + user: None, + }, channel: Some(Channel { bitrate: None, guild_id: None, @@ -135,6 +140,7 @@ mod tests { video_quality_mode: None, }), channel_id: Some(Id::new(2)), + context: None, data: Some(InteractionData::ApplicationCommand(Box::new(CommandData { guild_id: None, id: Id::new(5), @@ -195,6 +201,7 @@ mod tests { guild_id: Some(Id::new(1)), id: Id::new(4), interaction: None, + interaction_metadata: None, kind: MessageType::Regular, member: Some(PartialMember { avatar: None, @@ -218,15 +225,15 @@ mod tests { poll: None, reactions: Vec::new(), reference: None, + referenced_message: None, role_subscription_data: None, sticker_items: vec![MessageSticker { format_type: StickerFormatType::Png, id: Id::new(1), name: "sticker name".to_owned(), }], - referenced_message: None, - thread: None, timestamp, + thread: None, tts: false, webhook_id: None, }, diff --git a/twilight-cache-inmemory/src/event/message.rs b/twilight-cache-inmemory/src/event/message.rs index b136cd9ba12..6a8477b394a 100644 --- a/twilight-cache-inmemory/src/event/message.rs +++ b/twilight-cache-inmemory/src/event/message.rs @@ -102,6 +102,7 @@ mod tests { util::{image_hash::ImageHashParseError, ImageHash, Timestamp}, }; + #[allow(deprecated)] #[test] fn message_create() -> Result<(), ImageHashParseError> { let joined_at = Some(Timestamp::from_secs(1_632_072_645).expect("non zero")); @@ -147,6 +148,7 @@ mod tests { guild_id: Some(Id::new(1)), id: Id::new(4), interaction: None, + interaction_metadata: None, kind: MessageType::Regular, member: Some(PartialMember { avatar: None, @@ -170,11 +172,11 @@ mod tests { poll: None, reactions: Vec::new(), reference: None, + referenced_message: None, role_subscription_data: None, sticker_items: Vec::new(), - thread: None, - referenced_message: None, timestamp: Timestamp::from_secs(1_632_072_645).expect("non zero"), + thread: None, tts: false, webhook_id: None, }; diff --git a/twilight-cache-inmemory/src/model/message.rs b/twilight-cache-inmemory/src/model/message.rs index faf44efd6b6..a53ef1ebc02 100644 --- a/twilight-cache-inmemory/src/model/message.rs +++ b/twilight-cache-inmemory/src/model/message.rs @@ -301,6 +301,7 @@ impl CachedMessage { } impl From for CachedMessage { + #[allow(deprecated)] fn from(message: Message) -> Self { let Message { activity, @@ -318,6 +319,7 @@ impl From for CachedMessage { guild_id, id, interaction, + interaction_metadata: _, kind, member, mention_channels, @@ -376,6 +378,7 @@ impl From for CachedMessage { } impl PartialEq for CachedMessage { + #[allow(deprecated)] fn eq(&self, other: &Message) -> bool { self.id == other.id && self.activity == other.activity diff --git a/twilight-cache-inmemory/src/test.rs b/twilight-cache-inmemory/src/test.rs index c853e9c4bf6..c2b97a0df96 100644 --- a/twilight-cache-inmemory/src/test.rs +++ b/twilight-cache-inmemory/src/test.rs @@ -33,7 +33,7 @@ pub fn cache() -> DefaultInMemoryCache { DefaultInMemoryCache::new() } -#[allow(clippy::too_many_lines)] +#[allow(clippy::too_many_lines, deprecated)] pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { let joined_at = Some(Timestamp::from_secs(1_632_072_645).expect("non zero")); let cache = DefaultInMemoryCache::new(); @@ -75,6 +75,7 @@ pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { guild_id: Some(Id::new(1)), id: Id::new(4), interaction: None, + interaction_metadata: None, kind: MessageType::Regular, member: Some(PartialMember { avatar: None, @@ -98,11 +99,11 @@ pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { poll: None, reactions: Vec::new(), reference: None, + referenced_message: None, role_subscription_data: None, sticker_items: Vec::new(), - thread: None, - referenced_message: None, timestamp: Timestamp::from_secs(1_632_072_645).expect("non zero"), + thread: None, tts: false, webhook_id: None, }; diff --git a/twilight-http/src/request/application/command/mod.rs b/twilight-http/src/request/application/command/mod.rs index a683256544a..437cfa896c3 100644 --- a/twilight-http/src/request/application/command/mod.rs +++ b/twilight-http/src/request/application/command/mod.rs @@ -76,9 +76,11 @@ mod tests { /// `Command` or a type is changed then the destructure of it and creation /// of `CommandBorrowed` will fail. #[test] + #[allow(deprecated)] fn command_borrowed_from_command() { let command = Command { application_id: Some(Id::new(1)), + contexts: None, default_member_permissions: Some(Permissions::ADMINISTRATOR), dm_permission: Some(true), description: "command description".to_owned(), @@ -88,6 +90,7 @@ mod tests { )])), guild_id: Some(Id::new(2)), id: Some(Id::new(3)), + integration_types: None, kind: CommandType::ChatInput, name: "command name".to_owned(), name_localizations: Some(HashMap::from([( diff --git a/twilight-http/src/request/update_user_application.rs b/twilight-http/src/request/update_user_application.rs index be1f44608c2..aae9d4ea604 100644 --- a/twilight-http/src/request/update_user_application.rs +++ b/twilight-http/src/request/update_user_application.rs @@ -1,7 +1,10 @@ use std::future::IntoFuture; use serde::Serialize; -use twilight_model::oauth::{Application, ApplicationFlags, InstallParams}; +use twilight_model::oauth::{ + Application, ApplicationFlags, ApplicationIntegrationMap, ApplicationIntegrationTypeConfig, + InstallParams, +}; use crate::{ client::Client, @@ -26,6 +29,8 @@ struct UpdateCurrentUserApplicationFields<'a> { #[serde(skip_serializing_if = "Option::is_none")] install_params: Option, #[serde(skip_serializing_if = "Option::is_none")] + integration_types_config: Option>, + #[serde(skip_serializing_if = "Option::is_none")] interactions_endpoint_url: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] role_connections_verification_url: Option<&'a str>, @@ -77,6 +82,7 @@ impl<'a> UpdateCurrentUserApplication<'a> { flags: None, icon: None, install_params: None, + integration_types_config: None, interactions_endpoint_url: None, role_connections_verification_url: None, tags: None, @@ -129,6 +135,24 @@ impl<'a> UpdateCurrentUserApplication<'a> { self } + pub fn integrations_types_config( + mut self, + guild: Option, + user: Option, + ) -> Self { + let guild = guild.map(|g| ApplicationIntegrationTypeConfig { + oauth2_install_params: Some(g), + }); + + let user = user.map(|u| ApplicationIntegrationTypeConfig { + oauth2_install_params: Some(u), + }); + + self.fields.integration_types_config = Some(ApplicationIntegrationMap { guild, user }); + + self + } + /// Sets the interactions endpoint URL of the application. pub const fn interactions_endpoint_url(mut self, interactions_endpoint_url: &'a str) -> Self { self.fields.interactions_endpoint_url = Some(interactions_endpoint_url); diff --git a/twilight-model/benches/deserialization.rs b/twilight-model/benches/deserialization.rs index 093958d2261..0c18b148440 100644 --- a/twilight-model/benches/deserialization.rs +++ b/twilight-model/benches/deserialization.rs @@ -34,6 +34,7 @@ fn member_chunk() { "members": [{ "deaf": false, "hoisted_role": "6", + "flags": 0, "joined_at": "2020-04-04T04:04:04.000000+00:00", "mute": false, "nick": "chunk", @@ -48,6 +49,7 @@ fn member_chunk() { }, { "deaf": false, "hoisted_role": "6", + "flags": 0, "joined_at": "2020-04-04T04:04:04.000000+00:00", "mute": false, "nick": "chunk", @@ -61,6 +63,7 @@ fn member_chunk() { }, { "deaf": false, "hoisted_role": "6", + "flags": 0, "joined_at": "2020-04-04T04:04:04.000000+00:00", "mute": false, "nick": "chunk", @@ -75,6 +78,7 @@ fn member_chunk() { }, { "deaf": false, "hoisted_role": "6", + "flags": 0, "joined_at": "2020-04-04T04:04:04.000000+00:00", "mute": false, "nick": "chunk", @@ -134,6 +138,7 @@ fn reaction() { "member": { "deaf": false, "hoisted_role": "5", + "flags": 0, "joined_at": "2020-01-01T00:00:00.000000+00:00", "mute": false, "nick": "typing", @@ -159,6 +164,7 @@ fn typing_start() { "member": { "deaf": false, "hoisted_role": "4", + "flags": 0, "joined_at": "2020-01-01T00:00:00.000000+00:00", "mute": false, "nick": "typing", diff --git a/twilight-model/src/application/command/mod.rs b/twilight-model/src/application/command/mod.rs index 056997f9162..ab1f642b26d 100644 --- a/twilight-model/src/application/command/mod.rs +++ b/twilight-model/src/application/command/mod.rs @@ -26,10 +26,13 @@ use crate::{ marker::{ApplicationMarker, CommandMarker, CommandVersionMarker, GuildMarker}, Id, }, + oauth::ApplicationIntegrationType, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use super::interaction::InteractionContextType; + /// Data sent to Discord to create a command. /// /// [`CommandOption`]s that are required must be listed before optional ones. @@ -41,6 +44,8 @@ use std::collections::HashMap; pub struct Command { #[serde(skip_serializing_if = "Option::is_none")] pub application_id: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub contexts: Option>, /// Default permissions required for a member to run the command. /// /// Setting this [`Permissions::empty()`] will prohibit anyone from running @@ -50,6 +55,7 @@ pub struct Command { /// /// This is only relevant for globally-scoped commands. By default, commands /// are visible in DMs. + #[deprecated(note = "use contexts instead")] #[serde(skip_serializing_if = "Option::is_none")] pub dm_permission: Option, /// Description of the command. @@ -71,6 +77,8 @@ pub struct Command { pub guild_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub id: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub integration_types: Option>, #[serde(rename = "type")] pub kind: CommandType, pub name: String, @@ -105,10 +113,11 @@ mod tests { use std::collections::HashMap; #[test] - #[allow(clippy::too_many_lines)] + #[allow(clippy::too_many_lines, deprecated)] fn command_option_full() { let value = Command { application_id: Some(Id::new(100)), + contexts: None, default_member_permissions: Some(Permissions::ADMINISTRATOR), dm_permission: Some(false), description: "this command is a test".into(), @@ -118,6 +127,7 @@ mod tests { )])), guild_id: Some(Id::new(300)), id: Some(Id::new(200)), + integration_types: None, kind: CommandType::ChatInput, name: "test command".into(), name_localizations: Some(HashMap::from([("en-US".into(), "test command".into())])), diff --git a/twilight-model/src/application/interaction/context_type.rs b/twilight-model/src/application/interaction/context_type.rs new file mode 100644 index 00000000000..5150d7b7af4 --- /dev/null +++ b/twilight-model/src/application/interaction/context_type.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(from = "u8", into = "u8")] +pub enum InteractionContextType { + /// Interaction can be used within servers. + Guild, + /// Interaction can be used within DMs with the app's bot user. + BotDm, + /// Interaction can be used within Group DMs and DMs other than + /// the app's bot user. + PrivateChannel, + /// Variant value is unknown to the library. + Unknown(u8), +} + +impl InteractionContextType { + pub const fn kind(self) -> &'static str { + match self { + Self::Guild => "GUILD", + Self::BotDm => "BOT_DM", + Self::PrivateChannel => "PRIVATE_CHANNEL", + Self::Unknown(_) => "Unknown", + } + } +} + +impl From for InteractionContextType { + fn from(value: u8) -> Self { + match value { + 0 => Self::Guild, + 1 => Self::BotDm, + 2 => Self::PrivateChannel, + unknown => Self::Unknown(unknown), + } + } +} + +impl From for u8 { + fn from(value: InteractionContextType) -> Self { + match value { + InteractionContextType::Guild => 0, + InteractionContextType::BotDm => 1, + InteractionContextType::PrivateChannel => 2, + InteractionContextType::Unknown(unknown) => unknown, + } + } +} diff --git a/twilight-model/src/application/interaction/metadata.rs b/twilight-model/src/application/interaction/metadata.rs new file mode 100644 index 00000000000..a1e12b5ce12 --- /dev/null +++ b/twilight-model/src/application/interaction/metadata.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + id::{ + marker::{GuildMarker, InteractionMarker, MessageMarker, UserMarker}, + AnonymizableId, Id, + }, + oauth::ApplicationIntegrationMap, +}; + +use super::InteractionType; + +/// Structure containing metadata for interactions. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct InteractionMetadata { + /// IDs for installation context(s) related to an interaction. + pub authorizing_integration_owners: + ApplicationIntegrationMap, Id>, + /// ID of the interaction. + pub id: Id, + /// ID of the message that contained interactive component, present only on + /// messages created from component interactions + #[serde(skip_serializing_if = "Option::is_none")] + pub interacted_message_id: Option>, + /// Type of interaction. + #[serde(rename = "type")] + pub kind: InteractionType, + /// ID of the original response message, present only on follow-up messages. + #[serde(skip_serializing_if = "Option::is_none")] + pub original_response_message_id: Option>, + /// Metadata for the interaction that was used to open the modal, + /// present only on modal submit interactions + // This field cannot be in the nested interaction metadata. + #[serde(skip_serializing_if = "Option::is_none")] + pub triggering_interaction_metadata: Option>, +} diff --git a/twilight-model/src/application/interaction/mod.rs b/twilight-model/src/application/interaction/mod.rs index 67ac3dbb6c8..2079681237c 100644 --- a/twilight-model/src/application/interaction/mod.rs +++ b/twilight-model/src/application/interaction/mod.rs @@ -8,11 +8,15 @@ pub mod application_command; pub mod message_component; pub mod modal; +mod context_type; mod interaction_type; +mod metadata; mod resolved; pub use self::{ + context_type::InteractionContextType, interaction_type::InteractionType, + metadata::InteractionMetadata, resolved::{InteractionChannel, InteractionDataResolved, InteractionMember}, }; @@ -25,8 +29,9 @@ use crate::{ guild::{PartialMember, Permissions}, id::{ marker::{ApplicationMarker, ChannelMarker, GuildMarker, InteractionMarker, UserMarker}, - Id, + AnonymizableId, Id, }, + oauth::ApplicationIntegrationMap, user::User, }; use serde::{ @@ -46,12 +51,14 @@ use super::monetization::Entitlement; #[derive(Clone, Debug, PartialEq, Serialize)] pub struct Interaction { /// App's permissions in the channel the interaction was sent from. - /// - /// Present when the interaction is invoked in a guild. #[serde(skip_serializing_if = "Option::is_none")] pub app_permissions: Option, /// ID of the associated application. pub application_id: Id, + /// Mapping of installation contexts that the interaction was + /// authorized for to related user or guild IDs. + pub authorizing_integration_owners: + ApplicationIntegrationMap, Id>, /// The channel the interaction was invoked in. /// /// Present on all interactions types, except [`Ping`]. @@ -69,6 +76,9 @@ pub struct Interaction { note = "channel_id is deprecated in the discord API and will no be sent in the future, users should use the channel field instead." )] pub channel_id: Option>, + /// Context where the interaction was triggered from. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, /// Data from the interaction. /// /// This field present on [`ApplicationCommand`], [`MessageComponent`], @@ -178,6 +188,7 @@ impl<'de> Deserialize<'de> for Interaction { enum InteractionField { AppPermissions, ApplicationId, + Context, Channel, ChannelId, Data, @@ -192,6 +203,7 @@ enum InteractionField { Type, User, Version, + AuthorizingIntegrationOwners, } struct InteractionVisitor; @@ -209,6 +221,7 @@ impl<'de> Visitor<'de> for InteractionVisitor { let mut application_id: Option> = None; let mut channel: Option = None; let mut channel_id: Option> = None; + let mut context: Option = None; let mut data: Option = None; let mut entitlements: Option> = None; let mut guild_id: Option> = None; @@ -220,6 +233,9 @@ impl<'de> Visitor<'de> for InteractionVisitor { let mut message: Option = None; let mut token: Option = None; let mut user: Option = None; + let mut authorizing_integration_owners: Option< + ApplicationIntegrationMap, Id>, + > = None; loop { let key = match map.next_key() { @@ -247,6 +263,13 @@ impl<'de> Visitor<'de> for InteractionVisitor { application_id = Some(map.next_value()?); } + InteractionField::Context => { + if context.is_some() { + return Err(DeError::duplicate_field("context")); + } + + context = map.next_value()?; + } InteractionField::Channel => { if channel.is_some() { return Err(DeError::duplicate_field("channel")); @@ -342,11 +365,20 @@ impl<'de> Visitor<'de> for InteractionVisitor { // Ignoring the version field. map.next_value::()?; } + InteractionField::AuthorizingIntegrationOwners => { + if authorizing_integration_owners.is_some() { + return Err(DeError::duplicate_field("authorizing_integration_owners")); + } + + authorizing_integration_owners = map.next_value()?; + } } } let application_id = application_id.ok_or_else(|| DeError::missing_field("application_id"))?; + let authorizing_integration_owners = authorizing_integration_owners + .ok_or_else(|| DeError::missing_field("authorizing_integration_owners"))?; let id = id.ok_or_else(|| DeError::missing_field("id"))?; let token = token.ok_or_else(|| DeError::missing_field("token"))?; let kind = kind.ok_or_else(|| DeError::missing_field("kind"))?; @@ -392,8 +424,10 @@ impl<'de> Visitor<'de> for InteractionVisitor { Ok(Self::Value { app_permissions, application_id, + authorizing_integration_owners, channel, channel_id, + context, data, entitlements, guild_id, @@ -444,6 +478,7 @@ mod tests { channel::Channel, guild::{MemberFlags, PartialMember, Permissions}, id::Id, + oauth::ApplicationIntegrationMap, test::image_hash, user::User, util::datetime::{Timestamp, TimestampParseError}, @@ -460,6 +495,10 @@ mod tests { let value = Interaction { app_permissions: Some(Permissions::SEND_MESSAGES), application_id: Id::new(100), + authorizing_integration_owners: ApplicationIntegrationMap { + guild: None, + user: None, + }, channel: Some(Channel { bitrate: None, guild_id: None, @@ -498,6 +537,7 @@ mod tests { video_quality_mode: None, }), channel_id: Some(Id::new(200)), + context: None, data: Some(InteractionData::ApplicationCommand(Box::new(CommandData { guild_id: None, id: Id::new(300), @@ -614,7 +654,7 @@ mod tests { &[ Token::Struct { name: "Interaction", - len: 13, + len: 14, }, Token::Str("app_permissions"), Token::Some, @@ -622,6 +662,12 @@ mod tests { Token::Str("application_id"), Token::NewtypeStruct { name: "Id" }, Token::Str("100"), + Token::Str("authorizing_integration_owners"), + Token::Struct { + name: "ApplicationIntegrationMap", + len: 0, + }, + Token::StructEnd, Token::Str("channel"), Token::Some, Token::Struct { diff --git a/twilight-model/src/application/interaction/resolved.rs b/twilight-model/src/application/interaction/resolved.rs index 5511d3bcd86..fce75b9e4cd 100644 --- a/twilight-model/src/application/interaction/resolved.rs +++ b/twilight-model/src/application/interaction/resolved.rs @@ -116,7 +116,7 @@ mod tests { use std::str::FromStr; #[test] - #[allow(clippy::too_many_lines)] + #[allow(clippy::too_many_lines, deprecated)] fn test_data_resolved() -> Result<(), TimestampParseError> { let joined_at = Some(Timestamp::from_str("2021-08-10T12:18:37.000000+00:00")?); let timestamp = Timestamp::from_str("2020-02-02T02:02:02.020000+00:00")?; @@ -207,6 +207,7 @@ mod tests { guild_id: Some(Id::new(1)), id: Id::new(4), interaction: None, + interaction_metadata: None, kind: MessageType::Regular, member: Some(PartialMember { avatar: None, @@ -230,15 +231,15 @@ mod tests { poll: None, reactions: Vec::new(), reference: None, + referenced_message: None, role_subscription_data: None, sticker_items: vec![MessageSticker { format_type: StickerFormatType::Png, id: Id::new(1), name: "sticker name".to_owned(), }], - referenced_message: None, - thread: None, timestamp, + thread: None, tts: false, webhook_id: None, }, diff --git a/twilight-model/src/channel/message/mod.rs b/twilight-model/src/channel/message/mod.rs index af85fc179ce..079dc37209b 100644 --- a/twilight-model/src/channel/message/mod.rs +++ b/twilight-model/src/channel/message/mod.rs @@ -41,6 +41,7 @@ pub use self::{ }; use crate::{ + application::interaction::InteractionMetadata, channel::{Attachment, Channel, ChannelMention}, guild::PartialMember, id::{ @@ -143,8 +144,13 @@ pub struct Message { /// Id of the message. pub id: Id, /// Interaction the message was sent as a response to. + #[deprecated(note = "use interaction_metadata instead")] #[serde(skip_serializing_if = "Option::is_none")] pub interaction: Option, + /// Contains metadata related to the interacting if the message is + /// sent as a result of an interaction. + #[serde(skip_serializing_if = "Option::is_none")] + pub interaction_metadata: Option>, /// Type of message. #[serde(rename = "type")] pub kind: MessageType, @@ -230,7 +236,7 @@ mod tests { use serde_test::Token; use std::str::FromStr; - #[allow(clippy::too_many_lines)] + #[allow(clippy::too_many_lines, deprecated)] #[test] fn message_deserialization() { let joined_at = Some(Timestamp::from_str("2020-01-01T00:00:00.000000+00:00").unwrap()); @@ -309,6 +315,7 @@ mod tests { thread: None, tts: false, webhook_id: None, + interaction_metadata: None, }; serde_test::assert_tokens( @@ -444,7 +451,7 @@ mod tests { ); } - #[allow(clippy::too_many_lines)] + #[allow(clippy::too_many_lines, deprecated)] #[test] fn message_deserialization_complete() -> Result<(), TimestampParseError> { let edited_timestamp = Timestamp::from_str("2021-08-10T12:41:51.602000+00:00")?; @@ -553,6 +560,7 @@ mod tests { thread: None, tts: false, webhook_id: Some(Id::new(1)), + interaction_metadata: None, }; serde_test::assert_tokens( diff --git a/twilight-model/src/guild/permissions.rs b/twilight-model/src/guild/permissions.rs index d449ee62c45..9d5de9705bb 100644 --- a/twilight-model/src/guild/permissions.rs +++ b/twilight-model/src/guild/permissions.rs @@ -79,8 +79,9 @@ bitflags! { const SEND_VOICE_MESSAGES = 1 << 46; /// Allows sending polls. const SEND_POLLS = 1 << 49; - /// Allows user-installed apps to send public responses. When disabled, users will still - /// be allowed to use their apps but the responses will be ephemeral. This only applies to + /// Allows user-installed apps to send public responses. When + /// disabled, users will still be allowed to use their apps + /// but the responses will be ephemeral. This only applies to /// apps not also installed to the server. const USE_EXTERNAL_APPS = 1 << 50; } diff --git a/twilight-model/src/oauth/application.rs b/twilight-model/src/oauth/application.rs index 441ffc70011..afff4f40c6a 100644 --- a/twilight-model/src/oauth/application.rs +++ b/twilight-model/src/oauth/application.rs @@ -1,4 +1,8 @@ -use super::{team::Team, ApplicationFlags, InstallParams}; +use super::{ + application_integration_type::{ApplicationIntegrationMap, ApplicationIntegrationTypeConfig}, + team::Team, + ApplicationFlags, InstallParams, +}; use crate::{ guild::Guild, id::{ @@ -15,24 +19,33 @@ pub struct Application { /// Approximate count of guilds this app has been added to. #[serde(skip_serializing_if = "Option::is_none")] pub approximate_guild_count: Option, + /// Approximate count of users that have installed the app. + #[serde(skip_serializing_if = "Option::is_none")] + pub approximate_user_install_count: Option, /// Partial user object for the bot user associated with the app. #[serde(skip_serializing_if = "Option::is_none")] pub bot: Option, + /// When `false`, only the app owner can add the app to guilds pub bot_public: bool, + /// When `true`, the app's bot will only join upon completion of the + /// full OAuth2 code grant flow pub bot_require_code_grant: bool, /// Default rich presence invite cover image. + #[serde(skip_serializing_if = "Option::is_none")] pub cover_image: Option, /// Application's default custom authorization link, if enabled. #[serde(skip_serializing_if = "Option::is_none")] pub custom_install_url: Option, /// Description of the application. pub description: String, - pub guild_id: Option>, + /// Public flags of the application. + pub flags: Option, /// Partial object of the associated guild. #[serde(skip_serializing_if = "Option::is_none")] pub guild: Option, - /// Public flags of the application. - pub flags: Option, + /// Guild associated with the app. For example, a developer support server. + #[serde(skip_serializing_if = "Option::is_none")] + pub guild_id: Option>, /// Icon of the application. pub icon: Option, /// ID of the application. @@ -40,28 +53,41 @@ pub struct Application { /// Settings for the application's default in-app authorization, if enabled. #[serde(skip_serializing_if = "Option::is_none")] pub install_params: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub integration_types_config: + Option>, /// Interactions endpoint URL for the app. #[serde(skip_serializing_if = "Option::is_none")] pub interactions_endpoint_url: Option, /// Name of the application. pub name: String, + /// Partial user object for the owner of the app. + #[serde(skip_serializing_if = "Option::is_none")] pub owner: Option, + /// If this app is a game sold on Discord, this field will be the + /// id of the "Game SKU" that is created, if exists. + #[serde(skip_serializing_if = "Option::is_none")] pub primary_sku_id: Option>, /// URL of the application's privacy policy. #[serde(skip_serializing_if = "Option::is_none")] pub privacy_policy_url: Option, + /// Redirect URIs for the application. + #[serde(skip_serializing_if = "Option::is_none")] + pub redirect_uris: Option>, /// Role connection verification URL for the app. #[serde(skip_serializing_if = "Option::is_none")] pub role_connections_verification_url: Option, #[serde(default)] pub rpc_origins: Vec, - /// Redirect URIs for the application. + /// If this app is a game sold on Discord, this field will be the + /// URL slug that links to the store page. #[serde(skip_serializing_if = "Option::is_none")] - pub redirect_uris: Option>, pub slug: Option, /// Tags describing the content and functionality of the application. #[serde(skip_serializing_if = "Option::is_none")] pub tags: Option>, + /// If the app belongs to a team, this will be a list of the + /// members of that team. pub team: Option, /// URL of the application's terms of service. #[serde(skip_serializing_if = "Option::is_none")] @@ -116,18 +142,20 @@ mod tests { fn current_application_info() { let value = Application { approximate_guild_count: Some(2), + approximate_user_install_count: Some(5), bot: None, bot_public: true, bot_require_code_grant: false, cover_image: Some(image_hash::COVER), custom_install_url: None, description: "a pretty cool application".to_owned(), - guild_id: Some(Id::new(1)), - guild: None, flags: Some(ApplicationFlags::EMBEDDED), + guild: None, + guild_id: Some(Id::new(1)), icon: Some(image_hash::ICON), id: Id::new(2), install_params: None, + integration_types_config: None, interactions_endpoint_url: Some("https://interactions".into()), name: "cool application".to_owned(), owner: Some(User { @@ -178,11 +206,14 @@ mod tests { &[ Token::Struct { name: "Application", - len: 21, + len: 22, }, Token::Str("approximate_guild_count"), Token::Some, Token::U64(2), + Token::Str("approximate_user_install_count"), + Token::Some, + Token::U64(5), Token::Str("bot_public"), Token::Bool(true), Token::Str("bot_require_code_grant"), @@ -192,13 +223,13 @@ mod tests { Token::Str(image_hash::COVER_INPUT), Token::Str("description"), Token::Str("a pretty cool application"), + Token::Str("flags"), + Token::Some, + Token::U64(131_072), Token::Str("guild_id"), Token::Some, Token::NewtypeStruct { name: "Id" }, Token::Str("1"), - Token::Str("flags"), - Token::Some, - Token::U64(131_072), Token::Str("icon"), Token::Some, Token::Str(image_hash::ICON_INPUT), diff --git a/twilight-model/src/oauth/application_integration_type.rs b/twilight-model/src/oauth/application_integration_type.rs new file mode 100644 index 00000000000..c33acd64782 --- /dev/null +++ b/twilight-model/src/oauth/application_integration_type.rs @@ -0,0 +1,59 @@ +use serde::{Deserialize, Serialize}; + +use super::InstallParams; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(from = "u8", into = "u8")] +pub enum ApplicationIntegrationType { + GuildInstall, + UserInstall, + /// Variant value is unknown to the library. + Unknown(u8), +} + +impl ApplicationIntegrationType { + pub const fn kind(self) -> &'static str { + match self { + Self::GuildInstall => "GUILD_INSTALL", + Self::UserInstall => "USER_INSTALL", + Self::Unknown(_) => "Unknown", + } + } +} + +impl From for ApplicationIntegrationType { + fn from(value: u8) -> Self { + match value { + 0 => Self::GuildInstall, + 1 => Self::UserInstall, + unknown => Self::Unknown(unknown), + } + } +} + +impl From for u8 { + fn from(value: ApplicationIntegrationType) -> Self { + match value { + ApplicationIntegrationType::GuildInstall => 0, + ApplicationIntegrationType::UserInstall => 1, + ApplicationIntegrationType::Unknown(unknown) => unknown, + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct ApplicationIntegrationMap { + #[serde(rename = "0")] + #[serde(skip_serializing_if = "Option::is_none")] + pub guild: Option, + #[serde(rename = "1")] + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct ApplicationIntegrationTypeConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub oauth2_install_params: Option, +} diff --git a/twilight-model/src/oauth/current_authorization_information.rs b/twilight-model/src/oauth/current_authorization_information.rs index 4e3ccdbf47c..983483825ac 100644 --- a/twilight-model/src/oauth/current_authorization_information.rs +++ b/twilight-model/src/oauth/current_authorization_information.rs @@ -72,18 +72,20 @@ mod tests { let value = CurrentAuthorizationInformation { application: Application { approximate_guild_count: Some(2), + approximate_user_install_count: Some(4), bot: None, bot_public: true, bot_require_code_grant: true, cover_image: None, custom_install_url: None, description: DESCRIPTION.to_owned(), - guild_id: None, - guild: None, flags: None, + guild: None, + guild_id: None, icon: Some(image_hash::ICON), id: Id::new(100_000_000_000_000_000), install_params: None, + integration_types_config: None, interactions_endpoint_url: None, name: NAME.to_owned(), owner: None, @@ -113,21 +115,20 @@ mod tests { Token::Str("application"), Token::Struct { name: "Application", - len: 16, + len: 12, }, Token::Str("approximate_guild_count"), Token::Some, Token::U64(2), + Token::Str("approximate_user_install_count"), + Token::Some, + Token::U64(4), Token::Str("bot_public"), Token::Bool(true), Token::Str("bot_require_code_grant"), Token::Bool(true), - Token::Str("cover_image"), - Token::None, Token::Str("description"), Token::Str(DESCRIPTION), - Token::Str("guild_id"), - Token::None, Token::Str("flags"), Token::None, Token::Str("icon"), @@ -138,15 +139,9 @@ mod tests { Token::Str("100000000000000000"), Token::Str("name"), Token::Str(NAME), - Token::Str("owner"), - Token::None, - Token::Str("primary_sku_id"), - Token::None, Token::Str("rpc_origins"), Token::Seq { len: Some(0) }, Token::SeqEnd, - Token::Str("slug"), - Token::None, Token::Str("team"), Token::None, Token::Str("verify_key"), diff --git a/twilight-model/src/oauth/mod.rs b/twilight-model/src/oauth/mod.rs index 42720d2f27f..f056e2842e4 100644 --- a/twilight-model/src/oauth/mod.rs +++ b/twilight-model/src/oauth/mod.rs @@ -3,12 +3,18 @@ pub mod team; mod application; mod application_flags; +mod application_integration_type; mod current_authorization_information; mod install_params; mod partial_application; pub use self::{ - application::Application, application_flags::ApplicationFlags, + application::Application, + application_flags::ApplicationFlags, + application_integration_type::{ + ApplicationIntegrationMap, ApplicationIntegrationType, ApplicationIntegrationTypeConfig, + }, current_authorization_information::CurrentAuthorizationInformation, - install_params::InstallParams, partial_application::PartialApplication, + install_params::InstallParams, + partial_application::PartialApplication, }; diff --git a/twilight-standby/src/lib.rs b/twilight-standby/src/lib.rs index 4fef66fa384..93e7983e311 100644 --- a/twilight-standby/src/lib.rs +++ b/twilight-standby/src/lib.rs @@ -1075,13 +1075,14 @@ mod tests { }, guild::Permissions, id::{marker::GuildMarker, Id}, - oauth::{ApplicationFlags, PartialApplication}, + oauth::{ApplicationFlags, ApplicationIntegrationMap, PartialApplication}, user::{CurrentUser, User}, util::Timestamp, }; assert_impl_all!(Standby: Debug, Default, Send, Sync); + #[allow(deprecated)] fn message() -> Message { Message { activity: None, @@ -1118,6 +1119,7 @@ mod tests { guild_id: Some(Id::new(4)), id: Id::new(3), interaction: None, + interaction_metadata: None, kind: MessageType::Regular, member: None, mention_channels: Vec::new(), @@ -1160,6 +1162,10 @@ mod tests { Interaction { app_permissions: Some(Permissions::SEND_MESSAGES), application_id: Id::new(1), + authorizing_integration_owners: ApplicationIntegrationMap { + guild: None, + user: None, + }, channel: Some(Channel { bitrate: None, guild_id: None, @@ -1198,6 +1204,7 @@ mod tests { video_quality_mode: None, }), channel_id: None, + context: None, data: Some(InteractionData::MessageComponent(Box::new( MessageComponentInteractionData { custom_id: String::from("Click"), diff --git a/twilight-util/src/builder/command.rs b/twilight-util/src/builder/command.rs index 92e9a3f549f..50884b1eeb1 100644 --- a/twilight-util/src/builder/command.rs +++ b/twilight-util/src/builder/command.rs @@ -63,6 +63,7 @@ pub struct CommandBuilder(Command); impl CommandBuilder { /// Create a new default [`Command`] builder. #[must_use = "builders have no effect if unused"] + #[allow(deprecated)] pub fn new(name: impl Into, description: impl Into, kind: CommandType) -> Self { Self(Command { application_id: None, @@ -78,6 +79,8 @@ impl CommandBuilder { nsfw: None, options: Vec::new(), version: Id::new(1), + contexts: None, + integration_types: None, }) } @@ -124,6 +127,7 @@ impl CommandBuilder { /// Set whether the command is available in DMs. /// /// Defaults to [`None`]. + #[allow(deprecated)] pub const fn dm_permission(mut self, dm_permission: bool) -> Self { self.0.dm_permission = Some(dm_permission); @@ -1404,7 +1408,7 @@ mod tests { assert_impl_all!(UserBuilder: Clone, Debug, Send, Sync); #[test] - #[allow(clippy::too_many_lines)] + #[allow(clippy::too_many_lines, deprecated)] fn construct_command_with_builder() { let command = CommandBuilder::new( @@ -1455,16 +1459,18 @@ mod tests { let command_manual = Command { application_id: None, + contexts: None, default_member_permissions: None, dm_permission: None, description: String::from("Get or edit permissions for a user or a role"), + description_localizations: None, guild_id: None, id: None, + integration_types: None, kind: CommandType::ChatInput, name: String::from("permissions"), name_localizations: None, nsfw: Some(true), - description_localizations: None, options: Vec::from([ CommandOption { autocomplete: None, diff --git a/twilight-validate/src/command.rs b/twilight-validate/src/command.rs index 6d77aa0dded..9ded036e702 100644 --- a/twilight-validate/src/command.rs +++ b/twilight-validate/src/command.rs @@ -838,9 +838,11 @@ mod tests { // This tests [`description`] and [`name`] by proxy. #[test] + #[allow(deprecated)] fn command_length() { let valid_command = Command { application_id: Some(Id::new(1)), + contexts: None, default_member_permissions: None, dm_permission: None, description: "a".repeat(100), @@ -850,6 +852,7 @@ mod tests { )])), guild_id: Some(Id::new(2)), id: Some(Id::new(3)), + integration_types: None, kind: CommandType::ChatInput, name: "b".repeat(32), name_localizations: Some(HashMap::from([("en-US".to_string(), "b".repeat(32))])), @@ -908,6 +911,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn command_combined_limit() { let mut command = Command { application_id: Some(Id::new(1)), @@ -993,6 +997,8 @@ mod tests { required: None, }]), version: Id::new(4), + contexts: None, + integration_types: None, }; assert_eq!(command_characters(&command), 660);