Repository: serenity-rs/serenity (language: rust)

Issue:
Make user apps stable
A while ago, the user apps api on discord became stable, meaning we can put it into our stable API now without concerns of breakage.

Relevant changelog:
https://discord.com/developers/docs/change-log#userinstalled-apps-general-availability


Discussion / hints:

Fix patch:
diff --git a/src/builder/create_command.rs b/src/builder/create_command.rs
index 60a0278ff88..7f7da383a5b 100644
--- a/src/builder/create_command.rs
+++ b/src/builder/create_command.rs
@@ -313,10 +313,8 @@ pub struct CreateCommand {
     #[serde(skip_serializing_if = "Option::is_none")]
     #[serde(rename = "type")]
     kind: Option<CommandType>,
-    #[cfg(feature = "unstable_discord_api")]
     #[serde(skip_serializing_if = "Option::is_none")]
     integration_types: Option<Vec<InstallationContext>>,
-    #[cfg(feature = "unstable_discord_api")]
     #[serde(skip_serializing_if = "Option::is_none")]
     contexts: Option<Vec<InteractionContext>>,
     nsfw: bool,
@@ -335,9 +333,7 @@ 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(),
@@ -430,28 +426,24 @@ impl CreateCommand {
         self
     }
 
-    #[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<InstallationContext>) -> 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<InteractionContext>) -> Self {
         self.contexts = Some(contexts);
diff --git a/src/model/application/command.rs b/src/model/application/command.rs
index 7d2cd91e4f7..5bb6e891d5e 100644
--- a/src/model/application/command.rs
+++ b/src/model/application/command.rs
@@ -2,7 +2,6 @@ use std::collections::HashMap;
 
 use serde::Serialize;
 
-#[cfg(feature = "unstable_discord_api")]
 use super::{InstallationContext, InteractionContext};
 #[cfg(feature = "model")]
 use crate::builder::{Builder, CreateCommand};
@@ -86,14 +85,12 @@ pub struct Command {
     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")]
+    /// Defaults to [`InstallationContext::Guild`] and [`InstallationContext::User`].
     #[serde(default)]
     pub integration_types: Vec<InstallationContext>,
     /// 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<Vec<InteractionContext>>,
     /// 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 e90d95620d3..690a697f7eb 100644
--- a/src/model/application/command_interaction.rs
+++ b/src/model/application/command_interaction.rs
@@ -4,7 +4,6 @@ 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::{
@@ -86,10 +85,8 @@ pub struct CommandInteraction {
     pub entitlements: Vec<Entitlement>,
     /// 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<InteractionContext>,
 }
 
diff --git a/src/model/application/component_interaction.rs b/src/model/application/component_interaction.rs
index aef3953e351..01ea532d6ba 100644
--- a/src/model/application/component_interaction.rs
+++ b/src/model/application/component_interaction.rs
@@ -64,10 +64,8 @@ pub struct ComponentInteraction {
     pub entitlements: Vec<Entitlement>,
     /// 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<InteractionContext>,
 }
 
diff --git a/src/model/application/interaction.rs b/src/model/application/interaction.rs
index c58c94ecc57..0cd0b69b68d 100644
--- a/src/model/application/interaction.rs
+++ b/src/model/application/interaction.rs
@@ -1,20 +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 super::{
+    CommandInteraction,
+    ComponentInteraction,
+    InstallationContext,
+    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::id::{ApplicationId, GuildId, InteractionId, 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::utils::{deserialize_val, remove_from_map, StrOrInt};
 use crate::model::Permissions;
 
 /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object)
@@ -299,7 +299,6 @@ bitflags! {
 ///
 /// [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 {
@@ -315,12 +314,10 @@ pub enum AuthorizingIntegrationOwner {
 }
 
 #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
-#[cfg(feature = "unstable_discord_api")]
 #[derive(Clone, Debug, Default)]
 #[repr(transparent)]
 pub struct AuthorizingIntegrationOwners(pub Vec<AuthorizingIntegrationOwner>);
 
-#[cfg(feature = "unstable_discord_api")]
 impl<'de> serde::Deserialize<'de> for AuthorizingIntegrationOwners {
     fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
         struct Visitor;
@@ -366,7 +363,6 @@ impl<'de> serde::Deserialize<'de> for AuthorizingIntegrationOwners {
     }
 }
 
-#[cfg(feature = "unstable_discord_api")]
 impl serde::Serialize for AuthorizingIntegrationOwners {
     fn serialize<S: Serializer>(&self, serializer: S) -> StdResult<S::Ok, S::Error> {
         use serde::ser::SerializeMap;
@@ -419,28 +415,121 @@ pub struct MessageInteraction {
     pub member: Option<PartialMember>,
 }
 
-/// 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 {
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[non_exhaustive]
+pub struct MessageCommandInteractionMetadata {
     /// 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
+    /// The user who triggered the interaction
+    pub user: User,
+    /// The IDs for installation context(s) related to an interaction.
+    pub authorizing_integration_owners: AuthorizingIntegrationOwners,
+    /// The ID of the original response message, present only on follow-up messages.
+    pub original_response_message_id: Option<MessageId>,
+    /// The user the command was run on, present only on user command interactions
+    pub target_user: Option<User>,
+    /// The ID of the message the command was run on, present only on message command
+    /// interactions.
+    pub target_message_id: Option<MessageId>,
+}
+
+#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[non_exhaustive]
+pub struct MessageComponentInteractionMetadata {
+    /// The ID of the interaction
+    pub id: InteractionId,
+    /// The user who triggered the interaction
+    pub user: User,
+    /// The IDs for installation context(s) related to an interaction.
+    pub authorizing_integration_owners: AuthorizingIntegrationOwners,
+    /// The ID of the original response message, present only on follow-up messages.
+    pub original_response_message_id: Option<MessageId>,
+    /// The ID of the message that contained the interactive component
+    pub interacted_message_id: MessageId,
+}
+
+#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[non_exhaustive]
+pub struct MessageModalSubmitInteractionMetadata {
+    /// The ID of the interaction
+    pub id: InteractionId,
+    /// 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<MessageId>,
-    /// ID of the message that contained interactive component, present only on messages created
-    /// from component interactions.
-    pub interacted_message_id: Option<MessageId>,
-    /// Metadata for the interaction that was used to open the modal, present only on modal submit
-    /// interactions
-    pub triggering_interaction_metadata: Option<Box<MessageInteractionMetadata>>,
+    /// Metadata for the interaction that was used to open the modal
+    pub triggering_interaction_metadata: Box<MessageInteractionMetadata>,
+}
+
+/// 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)]
+#[non_exhaustive]
+pub enum MessageInteractionMetadata {
+    Command(MessageCommandInteractionMetadata),
+    Component(MessageComponentInteractionMetadata),
+    ModalSubmit(MessageModalSubmitInteractionMetadata),
+    Unknown(InteractionType),
+}
+
+impl<'de> serde::Deserialize<'de> for MessageInteractionMetadata {
+    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
+        let mut data = JsonMap::deserialize(deserializer)?;
+        let kind: InteractionType = remove_from_map(&mut data, "type")?;
+
+        match kind {
+            InteractionType::Command => deserialize_val(Value::from(data)).map(Self::Command),
+            InteractionType::Component => deserialize_val(Value::from(data)).map(Self::Component),
+            InteractionType::Modal => deserialize_val(Value::from(data)).map(Self::ModalSubmit),
+
+            unknown => Ok(Self::Unknown(unknown)),
+        }
+    }
+}
+
+impl serde::Serialize for MessageInteractionMetadata {
+    fn serialize<S: Serializer>(&self, serializer: S) -> StdResult<S::Ok, S::Error> {
+        #[derive(serde::Serialize)]
+        struct WithType<T> {
+            #[serde(rename = "type")]
+            kind: InteractionType,
+            #[serde(flatten)]
+            val: T,
+        }
+
+        fn serialize_with_type<S: Serializer, T: serde::Serialize>(
+            serializer: S,
+            val: T,
+            kind: InteractionType,
+        ) -> StdResult<S::Ok, S::Error> {
+            let wrapper = WithType {
+                kind,
+                val,
+            };
+
+            wrapper.serialize(serializer)
+        }
+
+        match self {
+            MessageInteractionMetadata::Command(val) => {
+                serialize_with_type(serializer, val, InteractionType::Command)
+            },
+            MessageInteractionMetadata::Component(val) => {
+                serialize_with_type(serializer, val, InteractionType::Component)
+            },
+            MessageInteractionMetadata::ModalSubmit(val) => {
+                serialize_with_type(serializer, val, InteractionType::Modal)
+            },
+            &MessageInteractionMetadata::Unknown(kind) => {
+                tracing::warn!("Tried to serialize MessageInteractionMetadata::Unknown({}), serialising null instead", u8::from(kind));
+                serializer.serialize_none()
+            },
+        }
+    }
 }
diff --git a/src/model/application/mod.rs b/src/model/application/mod.rs
index 2187dc3af5e..2ec7099d255 100644
--- a/src/model/application/mod.rs
+++ b/src/model/application/mod.rs
@@ -1,5 +1,7 @@
 //! Models about OAuth2 applications.
 
+use std::collections::HashMap;
+
 mod command;
 pub use command::*;
 mod command_interaction;
@@ -77,10 +79,8 @@ 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<String>,
-    #[cfg(feature = "unstable_discord_api")]
     #[serde(default)]
-    pub integration_types_config:
-        std::collections::HashMap<InstallationContext, InstallationContextConfig>,
+    pub integration_types_config: HashMap<InstallationContext, InstallationContextConfig>,
     pub approximate_guild_count: Option<u32>,
     pub approximate_user_install_count: Option<u32>,
     pub guild: Option<PartialGuild>,
@@ -99,7 +99,6 @@ impl CurrentApplicationInfo {
     }
 }
 
-#[cfg(feature = "unstable_discord_api")]
 enum_number! {
     /// An enum representing the [installation contexts].
     ///
@@ -115,7 +114,6 @@ enum_number! {
     }
 }
 
-#[cfg(feature = "unstable_discord_api")]
 enum_number! {
     /// An enum representing the different [interaction contexts].
     ///
@@ -138,7 +136,6 @@ enum_number! {
 /// 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: Option<InstallParams>,
diff --git a/src/model/channel/message.rs b/src/model/channel/message.rs
index e4b46ff2793..520ee942f7b 100644
--- a/src/model/channel/message.rs
+++ b/src/model/channel/message.rs
@@ -106,15 +106,11 @@ pub struct Message {
     pub flags: Option<MessageFlags>,
     /// The message that was replied to using this message.
     pub referenced_message: Option<Box<Message>>, // Boxed to avoid recursion
-    #[cfg_attr(
-        all(not(ignore_serenity_deprecated), feature = "unstable_discord_api"),
-        deprecated = "Use interaction_metadata"
-    )]
+    #[cfg_attr(not(ignore_serenity_deprecated), deprecated = "Use interaction_metadata")]
     pub interaction: Option<Box<MessageInteraction>>,
     /// Sent if the message is a response to an [`Interaction`].
     ///
     /// [`Interaction`]: crate::model::application::Interaction
-    #[cfg(feature = "unstable_discord_api")]
     pub interaction_metadata: Option<Box<MessageInteractionMetadata>>,
     /// The thread that was started from this message, includes thread member object.
     pub thread: Option<GuildChannel>,
diff --git a/src/model/event.rs b/src/model/event.rs
index 085f465cf44..10e67420756 100644
--- a/src/model/event.rs
+++ b/src/model/event.rs
@@ -525,13 +525,9 @@ pub struct MessageUpdateEvent {
     pub flags: Option<Option<MessageFlags>>,
     #[serde(default, deserialize_with = "deserialize_some")]
     pub referenced_message: Option<Option<Box<Message>>>,
-    #[cfg_attr(
-        all(not(ignore_serenity_deprecated), feature = "unstable_discord_api"),
-        deprecated = "Use interaction_metadata"
-    )]
+    #[cfg_attr(not(ignore_serenity_deprecated), deprecated = "Use interaction_metadata")]
     #[serde(default, deserialize_with = "deserialize_some")]
     pub interaction: Option<Option<Box<MessageInteraction>>>,
-    #[cfg(feature = "unstable_discord_api")]
     pub interaction_metadata: Option<Option<Box<MessageInteractionMetadata>>>,
     #[serde(default, deserialize_with = "deserialize_some")]
     pub thread: Option<Option<GuildChannel>>,
@@ -576,7 +572,6 @@ impl MessageUpdateEvent {
             flags,
             referenced_message,
             interaction,
-            #[cfg(feature = "unstable_discord_api")]
             interaction_metadata,
             thread,
             components,
@@ -615,7 +610,6 @@ 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) }


Test patch: