Skip to main content

serenity/model/application/
interaction.rs

1use serde::de::{Deserialize, Deserializer, Error as DeError};
2use serde::ser::{Serialize, Serializer};
3
4use super::{
5    CommandInteraction,
6    ComponentInteraction,
7    InstallationContext,
8    ModalInteraction,
9    PingInteraction,
10};
11use crate::internal::prelude::*;
12use crate::json::from_value;
13use crate::model::guild::PartialMember;
14use crate::model::id::{ApplicationId, GuildId, InteractionId, MessageId, UserId};
15use crate::model::monetization::Entitlement;
16use crate::model::user::User;
17use crate::model::utils::{deserialize_val, remove_from_map, StrOrInt};
18use crate::model::Permissions;
19
20/// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object)
21#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
22#[derive(Clone, Debug)]
23#[non_exhaustive]
24// breaking to change this on current, not sure if worth on next
25#[allow(clippy::large_enum_variant)]
26pub enum Interaction {
27    Ping(PingInteraction),
28    Command(CommandInteraction),
29    Autocomplete(CommandInteraction),
30    Component(ComponentInteraction),
31    Modal(ModalInteraction),
32}
33
34impl Interaction {
35    /// Gets the interaction Id.
36    #[must_use]
37    pub fn id(&self) -> InteractionId {
38        match self {
39            Self::Ping(i) => i.id,
40            Self::Command(i) | Self::Autocomplete(i) => i.id,
41            Self::Component(i) => i.id,
42            Self::Modal(i) => i.id,
43        }
44    }
45
46    /// Gets the interaction type
47    #[must_use]
48    pub fn kind(&self) -> InteractionType {
49        match self {
50            Self::Ping(_) => InteractionType::Ping,
51            Self::Command(_) => InteractionType::Command,
52            Self::Component(_) => InteractionType::Component,
53            Self::Autocomplete(_) => InteractionType::Autocomplete,
54            Self::Modal(_) => InteractionType::Modal,
55        }
56    }
57
58    /// Permissions the app or bot has within the channel the interaction was sent from.
59    #[must_use]
60    pub fn app_permissions(&self) -> Option<Permissions> {
61        match self {
62            Self::Ping(_) => None,
63            Self::Command(i) | Self::Autocomplete(i) => i.app_permissions,
64            Self::Component(i) => i.app_permissions,
65            Self::Modal(i) => i.app_permissions,
66        }
67    }
68
69    /// Guild ID the interaction was sent from, if any.
70    #[must_use]
71    pub fn guild_id(&self) -> Option<GuildId> {
72        match self {
73            Self::Ping(_) => None,
74            Self::Command(i) | Self::Autocomplete(i) => i.guild_id,
75            Self::Component(i) => i.guild_id,
76            Self::Modal(i) => i.guild_id,
77        }
78    }
79
80    /// Gets the interaction application Id
81    #[must_use]
82    pub fn application_id(&self) -> ApplicationId {
83        match self {
84            Self::Ping(i) => i.application_id,
85            Self::Command(i) | Self::Autocomplete(i) => i.application_id,
86            Self::Component(i) => i.application_id,
87            Self::Modal(i) => i.application_id,
88        }
89    }
90
91    /// Gets the interaction token.
92    #[must_use]
93    pub fn token(&self) -> &str {
94        match self {
95            Self::Ping(i) => i.token.as_str(),
96            Self::Command(i) | Self::Autocomplete(i) => i.token.as_str(),
97            Self::Component(i) => i.token.as_str(),
98            Self::Modal(i) => i.token.as_str(),
99        }
100    }
101
102    /// Gets the invoked guild locale.
103    #[must_use]
104    pub fn guild_locale(&self) -> Option<&str> {
105        match self {
106            Self::Ping(_) => None,
107            Self::Command(i) | Self::Autocomplete(i) => i.guild_locale.as_deref(),
108            Self::Component(i) => i.guild_locale.as_deref(),
109            Self::Modal(i) => i.guild_locale.as_deref(),
110        }
111    }
112
113    /// For monetized applications, gets the invoking user's granted entitlements.
114    #[must_use]
115    pub fn entitlements(&self) -> Option<&[Entitlement]> {
116        match self {
117            Self::Ping(_) => None,
118            Self::Command(i) | Self::Autocomplete(i) => Some(&i.entitlements),
119            Self::Component(i) => Some(&i.entitlements),
120            Self::Modal(i) => Some(&i.entitlements),
121        }
122    }
123
124    /// Converts this to a [`PingInteraction`]
125    #[must_use]
126    pub fn ping(self) -> Option<PingInteraction> {
127        match self {
128            Self::Ping(i) => Some(i),
129            _ => None,
130        }
131    }
132
133    /// Converts this to a [`PingInteraction`]
134    #[must_use]
135    pub fn as_ping(&self) -> Option<&PingInteraction> {
136        match self {
137            Self::Ping(i) => Some(i),
138            _ => None,
139        }
140    }
141
142    /// Converts this to a [`PingInteraction`]
143    #[must_use]
144    pub fn into_ping(self) -> Option<PingInteraction> {
145        self.ping()
146    }
147
148    /// Converts this to an [`CommandInteraction`]
149    #[must_use]
150    pub fn command(self) -> Option<CommandInteraction> {
151        match self {
152            Self::Command(i) => Some(i),
153            _ => None,
154        }
155    }
156
157    /// Converts this to an [`CommandInteraction`]
158    #[must_use]
159    pub fn as_command(&self) -> Option<&CommandInteraction> {
160        match self {
161            Self::Command(i) => Some(i),
162            _ => None,
163        }
164    }
165
166    /// Converts this to an [`CommandInteraction`]
167    #[must_use]
168    pub fn into_command(self) -> Option<CommandInteraction> {
169        self.command()
170    }
171
172    /// Converts this to a [`ComponentInteraction`]
173    #[must_use]
174    pub fn message_component(self) -> Option<ComponentInteraction> {
175        match self {
176            Self::Component(i) => Some(i),
177            _ => None,
178        }
179    }
180
181    /// Converts this to a [`ComponentInteraction`]
182    #[must_use]
183    pub fn as_message_component(&self) -> Option<&ComponentInteraction> {
184        match self {
185            Self::Component(i) => Some(i),
186            _ => None,
187        }
188    }
189
190    /// Converts this to a [`ComponentInteraction`]
191    #[must_use]
192    pub fn into_message_component(self) -> Option<ComponentInteraction> {
193        self.message_component()
194    }
195
196    /// Converts this to a [`CommandInteraction`]
197    #[must_use]
198    pub fn autocomplete(self) -> Option<CommandInteraction> {
199        match self {
200            Self::Autocomplete(i) => Some(i),
201            _ => None,
202        }
203    }
204
205    /// Converts this to a [`CommandInteraction`]
206    #[must_use]
207    pub fn as_autocomplete(&self) -> Option<&CommandInteraction> {
208        match self {
209            Self::Autocomplete(i) => Some(i),
210            _ => None,
211        }
212    }
213
214    /// Converts this to a [`CommandInteraction`]
215    #[must_use]
216    pub fn into_autocomplete(self) -> Option<CommandInteraction> {
217        self.autocomplete()
218    }
219
220    /// Converts this to a [`ModalInteraction`]
221    #[must_use]
222    pub fn modal_submit(self) -> Option<ModalInteraction> {
223        match self {
224            Self::Modal(i) => Some(i),
225            _ => None,
226        }
227    }
228
229    /// Converts this to a [`ModalInteraction`]
230    #[must_use]
231    pub fn as_modal_submit(&self) -> Option<&ModalInteraction> {
232        match self {
233            Self::Modal(i) => Some(i),
234            _ => None,
235        }
236    }
237
238    /// Converts this to a [`ModalInteraction`]
239    #[must_use]
240    pub fn into_modal_submit(self) -> Option<ModalInteraction> {
241        self.modal_submit()
242    }
243}
244
245// Manual impl needed to emulate integer enum tags
246impl<'de> Deserialize<'de> for Interaction {
247    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
248        let map = JsonMap::deserialize(deserializer)?;
249
250        let raw_kind = map.get("type").ok_or_else(|| DeError::missing_field("type"))?.clone();
251        let value = Value::from(map);
252
253        match deserialize_val(raw_kind)? {
254            InteractionType::Command => from_value(value).map(Interaction::Command),
255            InteractionType::Component => from_value(value).map(Interaction::Component),
256            InteractionType::Autocomplete => from_value(value).map(Interaction::Autocomplete),
257            InteractionType::Modal => from_value(value).map(Interaction::Modal),
258            InteractionType::Ping => from_value(value).map(Interaction::Ping),
259            InteractionType::Unknown(_) => return Err(DeError::custom("Unknown interaction type")),
260        }
261        .map_err(DeError::custom)
262    }
263}
264
265impl Serialize for Interaction {
266    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
267        match self {
268            Self::Ping(i) => i.serialize(serializer),
269            Self::Command(i) | Self::Autocomplete(i) => i.serialize(serializer),
270            Self::Component(i) => i.serialize(serializer),
271            Self::Modal(i) => i.serialize(serializer),
272        }
273    }
274}
275
276enum_number! {
277    /// The type of an Interaction.
278    ///
279    /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-type).
280    #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
281    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
282    #[serde(from = "u8", into = "u8")]
283    #[non_exhaustive]
284    pub enum InteractionType {
285        Ping = 1,
286        Command = 2,
287        Component = 3,
288        Autocomplete = 4,
289        Modal = 5,
290        _ => Unknown(u8),
291    }
292}
293
294bitflags! {
295    /// The flags for an interaction response message.
296    ///
297    /// [Discord docs](https://discord.com/developers/docs/resources/channel#message-object-message-flags)
298    /// ([only some are valid in this context](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-messages))
299    #[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq)]
300    pub struct InteractionResponseFlags: u64 {
301        /// Do not include any embeds when serializing this message.
302        const SUPPRESS_EMBEDS = 1 << 2;
303        /// Interaction message will only be visible to sender and will
304        /// be quickly deleted.
305        const EPHEMERAL = 1 << 6;
306        /// Does not trigger push notifications or desktop notifications.
307        const SUPPRESS_NOTIFICATIONS = 1 << 12;
308    }
309}
310
311/// A cleaned up enum for determining the authorizing owner for an [`Interaction`].
312///
313/// [Discord Docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object)
314#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
315#[derive(Clone, Debug)]
316#[non_exhaustive]
317pub enum AuthorizingIntegrationOwner {
318    /// The [`Application`] was installed to a guild, containing the id if invoked in said guild.
319    ///
320    /// [`Application`]: super::CurrentApplicationInfo
321    GuildInstall(Option<GuildId>),
322    /// The [`Application`] was installed to a user, containing the id of said user.
323    ///
324    /// [`Application`]: super::CurrentApplicationInfo
325    UserInstall(UserId),
326    Unknown(InstallationContext),
327}
328
329#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
330#[derive(Clone, Debug, Default)]
331#[repr(transparent)]
332pub struct AuthorizingIntegrationOwners(pub Vec<AuthorizingIntegrationOwner>);
333
334impl<'de> serde::Deserialize<'de> for AuthorizingIntegrationOwners {
335    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
336        struct Visitor;
337
338        impl<'de> serde::de::Visitor<'de> for Visitor {
339            type Value = AuthorizingIntegrationOwners;
340
341            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342                formatter.write_str("a hashmap containing keys of InstallationContext and values based on those keys")
343            }
344
345            fn visit_map<A>(self, mut map: A) -> StdResult<Self::Value, A::Error>
346            where
347                A: serde::de::MapAccess<'de>,
348            {
349                let mut out = Vec::new();
350                while let Some(key_str) = map.next_key::<serde_cow::CowStr<'_>>()? {
351                    let key_int = key_str.0.parse::<u8>().map_err(serde::de::Error::custom)?;
352                    let value = match InstallationContext::from(key_int) {
353                        InstallationContext::Guild => {
354                            // GuildId here can be `0`, which signals the command is guild installed
355                            // but invoked in a DM, we have to do this fun deserialisation dance.
356                            let id_str = map.next_value::<StrOrInt<'_>>()?;
357                            let id_int = id_str.parse().map_err(A::Error::custom)?;
358                            let id = std::num::NonZeroU64::new(id_int).map(GuildId::from);
359
360                            AuthorizingIntegrationOwner::GuildInstall(id)
361                        },
362                        InstallationContext::User => {
363                            AuthorizingIntegrationOwner::UserInstall(map.next_value()?)
364                        },
365                        key => AuthorizingIntegrationOwner::Unknown(key),
366                    };
367
368                    out.push(value);
369                }
370
371                Ok(AuthorizingIntegrationOwners(out))
372            }
373        }
374
375        deserializer.deserialize_map(Visitor)
376    }
377}
378
379impl serde::Serialize for AuthorizingIntegrationOwners {
380    fn serialize<S: Serializer>(&self, serializer: S) -> StdResult<S::Ok, S::Error> {
381        use serde::ser::SerializeMap;
382
383        let mut serializer = serializer.serialize_map(Some(self.0.len()))?;
384        for value in &self.0 {
385            match value {
386                AuthorizingIntegrationOwner::GuildInstall(inner) => {
387                    serializer.serialize_entry(&InstallationContext::Guild, &inner)
388                },
389                AuthorizingIntegrationOwner::UserInstall(inner) => {
390                    serializer.serialize_entry(&InstallationContext::User, &inner)
391                },
392                AuthorizingIntegrationOwner::Unknown(inner) => {
393                    serializer.serialize_entry(&inner, &())
394                },
395            }?;
396        }
397
398        serializer.end()
399    }
400}
401
402/// Sent when a [`Message`] is a response to an [`Interaction`].
403///
404/// [`Message`]: crate::model::channel::Message
405///
406/// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object).
407#[cfg_attr(
408    all(not(ignore_serenity_deprecated), feature = "unstable_discord_api"),
409    deprecated = "Use Message::interaction_metadata"
410)]
411#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
412#[derive(Clone, Debug, Deserialize, Serialize)]
413#[non_exhaustive]
414pub struct MessageInteraction {
415    /// The id of the interaction.
416    pub id: InteractionId,
417    /// The type of the interaction.
418    #[serde(rename = "type")]
419    pub kind: InteractionType,
420    /// The name of the [`Command`].
421    ///
422    /// [`Command`]: crate::model::application::Command
423    pub name: String,
424    /// The user who invoked the interaction.
425    pub user: User,
426    /// The member who invoked the interaction in the guild.
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub member: Option<PartialMember>,
429}
430
431#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
432#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
433#[non_exhaustive]
434pub struct MessageCommandInteractionMetadata {
435    /// The ID of the interaction
436    pub id: InteractionId,
437    /// The user who triggered the interaction
438    pub user: User,
439    /// The IDs for installation context(s) related to an interaction.
440    pub authorizing_integration_owners: AuthorizingIntegrationOwners,
441    /// The ID of the original response message, present only on follow-up messages.
442    pub original_response_message_id: Option<MessageId>,
443    /// The user the command was run on, present only on user command interactions
444    pub target_user: Option<User>,
445    /// The ID of the message the command was run on, present only on message command
446    /// interactions.
447    pub target_message_id: Option<MessageId>,
448}
449
450#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
451#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
452#[non_exhaustive]
453pub struct MessageComponentInteractionMetadata {
454    /// The ID of the interaction
455    pub id: InteractionId,
456    /// The user who triggered the interaction
457    pub user: User,
458    /// The IDs for installation context(s) related to an interaction.
459    pub authorizing_integration_owners: AuthorizingIntegrationOwners,
460    /// The ID of the original response message, present only on follow-up messages.
461    pub original_response_message_id: Option<MessageId>,
462    /// The ID of the message that contained the interactive component
463    pub interacted_message_id: MessageId,
464}
465
466#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
467#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
468#[non_exhaustive]
469pub struct MessageModalSubmitInteractionMetadata {
470    /// The ID of the interaction
471    pub id: InteractionId,
472    /// The user who triggered the interaction
473    pub user: User,
474    /// The IDs for installation context(s) related to an interaction.
475    pub authorizing_integration_owners: AuthorizingIntegrationOwners,
476    /// The ID of the original response message, present only on follow-up messages.
477    pub original_response_message_id: Option<MessageId>,
478    /// Metadata for the interaction that was used to open the modal
479    pub triggering_interaction_metadata: Box<MessageInteractionMetadata>,
480}
481
482/// Metadata about the interaction, including the source of the interaction relevant server and
483/// user IDs.
484#[allow(clippy::large_enum_variant)]
485#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
486#[derive(Clone, Debug)]
487#[non_exhaustive]
488pub enum MessageInteractionMetadata {
489    Command(MessageCommandInteractionMetadata),
490    Component(MessageComponentInteractionMetadata),
491    ModalSubmit(MessageModalSubmitInteractionMetadata),
492    Unknown(InteractionType),
493}
494
495impl<'de> serde::Deserialize<'de> for MessageInteractionMetadata {
496    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
497        let mut data = JsonMap::deserialize(deserializer)?;
498        let kind: InteractionType = remove_from_map(&mut data, "type")?;
499
500        match kind {
501            InteractionType::Command => deserialize_val(Value::from(data)).map(Self::Command),
502            InteractionType::Component => deserialize_val(Value::from(data)).map(Self::Component),
503            InteractionType::Modal => deserialize_val(Value::from(data)).map(Self::ModalSubmit),
504
505            unknown => Ok(Self::Unknown(unknown)),
506        }
507    }
508}
509
510impl serde::Serialize for MessageInteractionMetadata {
511    fn serialize<S: Serializer>(&self, serializer: S) -> StdResult<S::Ok, S::Error> {
512        #[derive(serde::Serialize)]
513        struct WithType<T> {
514            #[serde(rename = "type")]
515            kind: InteractionType,
516            #[serde(flatten)]
517            val: T,
518        }
519
520        fn serialize_with_type<S: Serializer, T: serde::Serialize>(
521            serializer: S,
522            val: T,
523            kind: InteractionType,
524        ) -> StdResult<S::Ok, S::Error> {
525            let wrapper = WithType {
526                kind,
527                val,
528            };
529
530            wrapper.serialize(serializer)
531        }
532
533        match self {
534            MessageInteractionMetadata::Command(val) => {
535                serialize_with_type(serializer, val, InteractionType::Command)
536            },
537            MessageInteractionMetadata::Component(val) => {
538                serialize_with_type(serializer, val, InteractionType::Component)
539            },
540            MessageInteractionMetadata::ModalSubmit(val) => {
541                serialize_with_type(serializer, val, InteractionType::Modal)
542            },
543            &MessageInteractionMetadata::Unknown(kind) => {
544                tracing::warn!("Tried to serialize MessageInteractionMetadata::Unknown({}), serialising null instead", u8::from(kind));
545                serializer.serialize_none()
546            },
547        }
548    }
549}