diff --git a/src/builder/create_command.rs b/src/builder/create_command.rs index aa659b5919a..60a0278ff88 100644 --- a/src/builder/create_command.rs +++ b/src/builder/create_command.rs @@ -313,6 +313,12 @@ pub struct CreateCommand { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "type")] kind: Option, + #[cfg(feature = "unstable_discord_api")] + #[serde(skip_serializing_if = "Option::is_none")] + integration_types: Option>, + #[cfg(feature = "unstable_discord_api")] + #[serde(skip_serializing_if = "Option::is_none")] + contexts: Option>, nsfw: bool, } @@ -329,6 +335,11 @@ impl CreateCommand { default_member_permissions: None, dm_permission: None, + #[cfg(feature = "unstable_discord_api")] + integration_types: None, + #[cfg(feature = "unstable_discord_api")] + contexts: None, + options: Vec::new(), nsfw: false, } @@ -372,6 +383,7 @@ impl CreateCommand { } /// Specifies if the command is available in DMs. + #[cfg_attr(feature = "unstable_discord_api", deprecated = "Use contexts instead")] pub fn dm_permission(mut self, enabled: bool) -> Self { self.dm_permission = Some(enabled); self @@ -418,7 +430,35 @@ impl CreateCommand { self } - /// Whether this channel is marked NSFW (age-restricted) + #[cfg(feature = "unstable_discord_api")] + /// Adds an installation context that this application command can be used in. + pub fn add_integration_type(mut self, integration_type: InstallationContext) -> Self { + self.integration_types.get_or_insert_with(Vec::default).push(integration_type); + self + } + + #[cfg(feature = "unstable_discord_api")] + /// Sets the installation contexts that this application command can be used in. + pub fn integration_types(mut self, integration_types: Vec) -> Self { + self.integration_types = Some(integration_types); + self + } + + #[cfg(feature = "unstable_discord_api")] + /// Adds an interaction context that this application command can be used in. + pub fn add_context(mut self, context: InteractionContext) -> Self { + self.contexts.get_or_insert_with(Vec::default).push(context); + self + } + + #[cfg(feature = "unstable_discord_api")] + /// Sets the interaction contexts that this application command can be used in. + pub fn contexts(mut self, contexts: Vec) -> Self { + self.contexts = Some(contexts); + self + } + + /// Whether this command is marked NSFW (age-restricted) pub fn nsfw(mut self, nsfw: bool) -> Self { self.nsfw = nsfw; self diff --git a/src/model/application/command.rs b/src/model/application/command.rs index 146adb19c4b..7232bd3f75e 100644 --- a/src/model/application/command.rs +++ b/src/model/application/command.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use serde::Serialize; +#[cfg(feature = "unstable_discord_api")] +use super::{InstallationContext, InteractionContext}; #[cfg(feature = "model")] use crate::builder::{Builder, CreateCommand}; #[cfg(feature = "model")] @@ -73,11 +75,23 @@ pub struct Command { /// Indicates whether the command is available in DMs with the app, only for globally-scoped /// commands. By default, commands are visible. #[serde(default)] + #[deprecated = "Use Command::contexts"] pub dm_permission: Option, /// Indicates whether the command is [age-restricted](https://discord.com/developers/docs/interactions/application-commands#agerestricted-commands), /// defaults to false. #[serde(default)] pub nsfw: bool, + /// Installation context(s) where the command is available, only for globally-scoped commands. + /// + /// Defaults to [`InstallationContext::Guild`] + #[cfg(feature = "unstable_discord_api")] + #[serde(default)] + pub integration_types: Vec, + /// Interaction context(s) where the command can be used, only for globally-scoped commands. + /// + /// By default, all interaction context types are included. + #[cfg(feature = "unstable_discord_api")] + pub contexts: Option>, /// An autoincremented version identifier updated during substantial record changes. pub version: CommandVersionId, } diff --git a/src/model/application/command_interaction.rs b/src/model/application/command_interaction.rs index 266412a2d0b..e90d95620d3 100644 --- a/src/model/application/command_interaction.rs +++ b/src/model/application/command_interaction.rs @@ -4,6 +4,8 @@ use serde::de::{Deserializer, Error as DeError}; use serde::ser::{Error as _, Serializer}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "unstable_discord_api")] +use super::{AuthorizingIntegrationOwners, InteractionContext}; #[cfg(feature = "model")] use crate::builder::{ Builder, @@ -74,6 +76,7 @@ pub struct CommandInteraction { /// Always `1`. pub version: u8, /// Permissions the app or bot has within the channel the interaction was sent from. + // TODO(next): This is now always serialized. pub app_permissions: Option, /// The selected language of the invoking user. pub locale: String, @@ -81,6 +84,13 @@ pub struct CommandInteraction { pub guild_locale: Option, /// For monetized applications, any entitlements of the invoking user. pub entitlements: Vec, + /// The owners of the applications that authorized the interaction, such as a guild or user. + #[serde(default)] + #[cfg(feature = "unstable_discord_api")] + pub authorizing_integration_owners: AuthorizingIntegrationOwners, + /// The context where the interaction was triggered from. + #[cfg(feature = "unstable_discord_api")] + pub context: Option, } #[cfg(feature = "model")] diff --git a/src/model/application/component_interaction.rs b/src/model/application/component_interaction.rs index a701f67b145..78442982edf 100644 --- a/src/model/application/component_interaction.rs +++ b/src/model/application/component_interaction.rs @@ -62,6 +62,13 @@ pub struct ComponentInteraction { pub guild_locale: Option, /// For monetized applications, any entitlements of the invoking user. pub entitlements: Vec, + /// The owners of the applications that authorized the interaction, such as a guild or user. + #[serde(default)] + #[cfg(feature = "unstable_discord_api")] + pub authorizing_integration_owners: AuthorizingIntegrationOwners, + /// The context where the interaction was triggered from. + #[cfg(feature = "unstable_discord_api")] + pub context: Option, } #[cfg(feature = "model")] diff --git a/src/model/application/interaction.rs b/src/model/application/interaction.rs index e97fd1fa211..fed83a2ed20 100644 --- a/src/model/application/interaction.rs +++ b/src/model/application/interaction.rs @@ -1,14 +1,20 @@ use serde::de::{Deserialize, Deserializer, Error as DeError}; use serde::ser::{Serialize, Serializer}; +#[cfg(feature = "unstable_discord_api")] +use super::InstallationContext; use super::{CommandInteraction, ComponentInteraction, ModalInteraction, PingInteraction}; use crate::internal::prelude::*; use crate::json::from_value; use crate::model::guild::PartialMember; use crate::model::id::{ApplicationId, InteractionId}; +#[cfg(feature = "unstable_discord_api")] +use crate::model::id::{GuildId, MessageId, UserId}; use crate::model::monetization::Entitlement; use crate::model::user::User; use crate::model::utils::deserialize_val; +#[cfg(feature = "unstable_discord_api")] +use crate::model::utils::StrOrInt; use crate::model::Permissions; /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object) @@ -289,11 +295,107 @@ bitflags! { } } +/// A cleaned up enum for determining the authorizing owner for an [`Interaction`]. +/// +/// [Discord Docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object) +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[cfg(feature = "unstable_discord_api")] +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum AuthorizingIntegrationOwner { + /// The [`Application`] was installed to a guild, containing the id if invoked in said guild. + /// + /// [`Application`]: super::CurrentApplicationInfo + GuildInstall(Option), + /// The [`Application`] was installed to a user, containing the id of said user. + /// + /// [`Application`]: super::CurrentApplicationInfo + UserInstall(UserId), + Unknown(InstallationContext), +} + +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[cfg(feature = "unstable_discord_api")] +#[derive(Clone, Debug, Default)] +#[repr(transparent)] +pub struct AuthorizingIntegrationOwners(Vec); + +#[cfg(feature = "unstable_discord_api")] +impl<'de> serde::Deserialize<'de> for AuthorizingIntegrationOwners { + fn deserialize>(deserializer: D) -> StdResult { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = AuthorizingIntegrationOwners; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a hashmap containing keys of InstallationContext and values based on those keys") + } + + fn visit_map(self, mut map: A) -> StdResult + where + A: serde::de::MapAccess<'de>, + { + let mut out = Vec::new(); + while let Some(key_str) = map.next_key::>()? { + let key_int = key_str.0.parse::().map_err(serde::de::Error::custom)?; + let value = match InstallationContext::from(key_int) { + InstallationContext::Guild => { + // GuildId here can be `0`, which signals the command is guild installed + // but invoked in a DM, we have to do this fun deserialisation dance. + let id_str = map.next_value::>()?; + let id_int = id_str.parse().map_err(A::Error::custom)?; + let id = std::num::NonZeroU64::new(id_int).map(GuildId::from); + + AuthorizingIntegrationOwner::GuildInstall(id) + }, + InstallationContext::User => { + AuthorizingIntegrationOwner::UserInstall(map.next_value()?) + }, + key => AuthorizingIntegrationOwner::Unknown(key), + }; + + out.push(value); + } + + Ok(AuthorizingIntegrationOwners(out)) + } + } + + deserializer.deserialize_map(Visitor) + } +} + +#[cfg(feature = "unstable_discord_api")] +impl serde::Serialize for AuthorizingIntegrationOwners { + fn serialize(&self, serializer: S) -> StdResult { + use serde::ser::SerializeMap; + + let mut serializer = serializer.serialize_map(Some(self.0.len()))?; + for value in &self.0 { + match value { + AuthorizingIntegrationOwner::GuildInstall(inner) => { + serializer.serialize_entry(&InstallationContext::Guild, &inner) + }, + AuthorizingIntegrationOwner::UserInstall(inner) => { + serializer.serialize_entry(&InstallationContext::User, &inner) + }, + AuthorizingIntegrationOwner::Unknown(inner) => { + serializer.serialize_entry(&inner, &()) + }, + }?; + } + + serializer.end() + } +} + /// Sent when a [`Message`] is a response to an [`Interaction`]. /// /// [`Message`]: crate::model::channel::Message /// /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object). +#[cfg_attr(not(ignore_serenity_deprecated), deprecated = "Use Message::interaction_metadata")] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] @@ -313,3 +415,29 @@ pub struct MessageInteraction { #[serde(skip_serializing_if = "Option::is_none")] pub member: Option, } + +/// Metadata about the interaction, including the source of the interaction relevant server and +/// user IDs. +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg(feature = "unstable_discord_api")] +pub struct MessageInteractionMetadata { + /// The ID of the interaction + pub id: InteractionId, + /// The type of interaction + #[serde(rename = "type")] + pub kind: InteractionType, + /// The ID of the user who triggered the interaction + pub user: User, + /// The IDs for installation context(s) related to an interaction. + #[serde(default)] + pub authorizing_integration_owners: AuthorizingIntegrationOwners, + /// The ID of the original response message, present only on follow-up messages. + pub original_response_message_id: Option, + /// ID of the message that contained interactive component, present only on messages created + /// from component interactions. + pub interacted_message_id: Option, + /// Metadata for the interaction that was used to open the modal, present only on modal submit + /// interactions + pub triggering_interaction_metadata: Option>, +} diff --git a/src/model/application/mod.rs b/src/model/application/mod.rs index 4f8a0a51686..769af479018 100644 --- a/src/model/application/mod.rs +++ b/src/model/application/mod.rs @@ -76,6 +76,55 @@ pub struct CurrentApplicationInfo { /// The application's role connection verification entry point, which when configured will /// render the app as a verification method in the guild role verification configuration. pub role_connections_verification_url: Option, + #[cfg(feature = "unstable_discord_api")] + #[serde(default)] + pub integration_types_config: + std::collections::HashMap, +} + +#[cfg(feature = "unstable_discord_api")] +enum_number! { + /// An enum representing the [installation contexts]. + /// + /// [interaction contexts](https://discord.com/developers/docs/resources/application#application-object-application-integration-types). + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] + #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] + #[serde(from = "u8", into = "u8")] + #[non_exhaustive] + pub enum InstallationContext { + Guild = 0, + User = 1, + _ => Unknown(u8), + } +} + +#[cfg(feature = "unstable_discord_api")] +enum_number! { + /// An enum representing the different [interaction contexts]. + /// + /// [interaction contexts](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-context-types). + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] + #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] + #[serde(from = "u8", into = "u8")] + #[non_exhaustive] + pub enum InteractionContext { + /// Interaction can be used within servers + Guild = 0, + /// Interaction can be used within DMs with the app's bot user + BotDm = 1, + /// Interaction can be used within Group DMs and DMs other than the app's bot user + PrivateChannel = 2, + _ => Unknown(u8), + } +} + +/// Information about how the [`CurrentApplicationInfo`] is installed. +/// +/// [Discord docs](https://discord.com/developers/docs/resources/application#application-object-application-integration-types). +#[cfg(feature = "unstable_discord_api")] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct InstallationContextConfig { + pub oauth2_install_params: InstallParams, } /// Information about the Team group of the application. diff --git a/src/model/channel/message.rs b/src/model/channel/message.rs index 4307c5e72c0..053f286b164 100644 --- a/src/model/channel/message.rs +++ b/src/model/channel/message.rs @@ -106,10 +106,14 @@ pub struct Message { pub flags: Option, /// The message that was replied to using this message. pub referenced_message: Option>, // Boxed to avoid recursion + #[cfg_attr(not(ignore_serenity_deprecated), deprecated = "Use interaction_metadata")] + #[allow(deprecated)] + pub interaction: Option>, /// Sent if the message is a response to an [`Interaction`]. /// /// [`Interaction`]: crate::model::application::Interaction - pub interaction: Option>, + #[cfg(feature = "unstable_discord_api")] + pub interaction_metadata: Option>, /// The thread that was started from this message, includes thread member object. pub thread: Option, /// The components of this message diff --git a/src/model/event.rs b/src/model/event.rs index 52a0c62199f..d49a74a9ff7 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -525,8 +525,12 @@ pub struct MessageUpdateEvent { pub flags: Option>, #[serde(default, deserialize_with = "deserialize_some")] pub referenced_message: Option>>, + #[cfg_attr(not(ignore_serenity_deprecated), deprecated = "Use interaction_metadata")] #[serde(default, deserialize_with = "deserialize_some")] + #[allow(deprecated)] pub interaction: Option>>, + #[cfg(feature = "unstable_discord_api")] + pub interaction_metadata: Option>>, #[serde(default, deserialize_with = "deserialize_some")] pub thread: Option>, pub components: Option>, @@ -570,6 +574,8 @@ impl MessageUpdateEvent { flags, referenced_message, interaction, + #[cfg(feature = "unstable_discord_api")] + interaction_metadata, thread, components, sticker_items, @@ -607,6 +613,8 @@ impl MessageUpdateEvent { if let Some(x) = flags { message.flags.clone_from(x) } if let Some(x) = referenced_message { message.referenced_message.clone_from(x) } if let Some(x) = interaction { message.interaction.clone_from(x) } + #[cfg(feature = "unstable_discord_api")] + if let Some(x) = interaction_metadata { message.interaction_metadata.clone_from(x) } if let Some(x) = thread { message.thread.clone_from(x) } if let Some(x) = components { message.components.clone_from(x) } if let Some(x) = sticker_items { message.sticker_items.clone_from(x) }