Skip to content

Commit

Permalink
Implement User Apps (serenity-rs#2807)
Browse files Browse the repository at this point in the history
Co-authored-by: Florian Gebhardt <me@fgardt.dev>
  • Loading branch information
GnomedDev and fgardt committed May 14, 2024
1 parent 5c7d8af commit 6a2c8e8
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 2 deletions.
42 changes: 41 additions & 1 deletion src/builder/create_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,12 @@ 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,
}

Expand All @@ -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,
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<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);
self
}

/// Whether this command is marked NSFW (age-restricted)
pub fn nsfw(mut self, nsfw: bool) -> Self {
self.nsfw = nsfw;
self
Expand Down
14 changes: 14 additions & 0 deletions src/model/application/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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<bool>,
/// 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<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,
}
Expand Down
10 changes: 10 additions & 0 deletions src/model/application/command_interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -74,13 +76,21 @@ 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<Permissions>,
/// The selected language of the invoking user.
pub locale: String,
/// The guild's preferred locale.
pub guild_locale: Option<String>,
/// For monetized applications, any entitlements of the invoking user.
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>,
}

#[cfg(feature = "model")]
Expand Down
7 changes: 7 additions & 0 deletions src/model/application/component_interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ pub struct ComponentInteraction {
pub guild_locale: Option<String>,
/// For monetized applications, any entitlements of the invoking user.
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>,
}

#[cfg(feature = "model")]
Expand Down
128 changes: 128 additions & 0 deletions src/model/application/interaction.rs
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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<GuildId>),
/// 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<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;

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<A>(self, mut map: A) -> StdResult<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut out = Vec::new();
while let Some(key_str) = map.next_key::<serde_cow::CowStr<'_>>()? {
let key_int = key_str.0.parse::<u8>().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::<StrOrInt<'_>>()?;
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<S: Serializer>(&self, serializer: S) -> StdResult<S::Ok, S::Error> {
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]
Expand All @@ -313,3 +415,29 @@ pub struct MessageInteraction {
#[serde(skip_serializing_if = "Option::is_none")]
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 {
/// 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<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>>,
}
49 changes: 49 additions & 0 deletions src/model/application/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
#[cfg(feature = "unstable_discord_api")]
#[serde(default)]
pub integration_types_config:
std::collections::HashMap<InstallationContext, InstallationContextConfig>,
}

#[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.
Expand Down
6 changes: 5 additions & 1 deletion src/model/channel/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,14 @@ 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(not(ignore_serenity_deprecated), deprecated = "Use interaction_metadata")]
#[allow(deprecated)]
pub interaction: Option<Box<MessageInteraction>>,
/// Sent if the message is a response to an [`Interaction`].
///
/// [`Interaction`]: crate::model::application::Interaction
pub interaction: Option<Box<MessageInteraction>>,
#[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>,
/// The components of this message
Expand Down
Loading

0 comments on commit 6a2c8e8

Please sign in to comment.