use serde::{Deserialize, Deserializer, de, de::DeserializeOwned};
use serde_json::value::RawValue;
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::types::{
ApplicationIntegrationType, Attachment, Channel, Entitlement, InteractionContextType, Lazy,
Member, Message, PartialChannel, PartialMember, Permissions, Role, Snowflake, SnowflakeMap,
User,
};
pub type ApplicationCommandInteraction = DataInteraction<ApplicationCommandInteractionData>;
pub type MessageComponentInteraction = DataInteraction<MessageComponentData>;
pub type ModalSubmitInteraction = DataInteraction<ModalSubmitData>;
#[derive(Debug, Clone)]
pub enum Interaction {
Ping(PingInteraction),
ApplicationCommand(ApplicationCommandInteraction),
MessageComponent(MessageComponentInteraction),
ApplicationCommandAutocomplete(ApplicationCommandInteraction),
ModalSubmit(ModalSubmitInteraction),
}
impl<'de> Deserialize<'de> for Interaction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let envelope = Envelope::deserialize(deserializer)?;
let common = InteractionCommon {
id: envelope.id,
application_id: envelope.application_id,
guild_id: envelope.guild_id,
channel: envelope.channel,
channel_id: envelope.channel_id,
member: envelope.member,
user: envelope.user,
token: envelope.token,
version: envelope.version,
message: envelope.message,
app_permissions: envelope.app_permissions,
guild_locale: envelope.guild_locale,
entitlements: envelope.entitlements,
authorizing_integration_owners: envelope.authorizing_integration_owners,
context: envelope.context,
};
let (locale, data) = (envelope.locale, envelope.data);
Ok(match envelope.t {
1 => Interaction::Ping(PingInteraction { common }),
2 => Interaction::ApplicationCommand(DataInteraction::new(common, locale, data)?),
3 => Interaction::MessageComponent(DataInteraction::new(common, locale, data)?),
4 => Interaction::ApplicationCommandAutocomplete(DataInteraction::new(
common, locale, data,
)?),
5 => Interaction::ModalSubmit(DataInteraction::new(common, locale, data)?),
_ => return Err(de::Error::custom("unknown interaction type")),
})
}
}
#[derive(Deserialize)]
struct Envelope {
#[serde(rename = "type")]
t: u8,
locale: Option<String>,
data: Option<Box<RawValue>>,
id: Snowflake,
application_id: Snowflake,
guild_id: Option<Snowflake>,
channel: Option<Lazy<Channel>>,
channel_id: Option<Snowflake>,
member: Option<Member>,
user: Option<User>,
token: String,
version: u8,
message: Option<Lazy<Message>>,
app_permissions: Option<Permissions>,
guild_locale: Option<String>,
#[serde(default)]
entitlements: Vec<Entitlement>,
#[serde(default)]
authorizing_integration_owners: IntegrationOwners,
context: Option<InteractionContextType>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct InteractionCommon {
pub id: Snowflake,
pub application_id: Snowflake,
pub guild_id: Option<Snowflake>,
pub channel: Option<Lazy<Channel>>,
pub channel_id: Option<Snowflake>,
pub member: Option<Member>,
pub user: Option<User>,
pub token: String,
pub version: u8,
pub message: Option<Lazy<Message>>,
pub app_permissions: Option<Permissions>,
pub guild_locale: Option<String>,
#[serde(default)]
pub authorizing_integration_owners: IntegrationOwners,
pub context: Option<InteractionContextType>,
#[serde(default)]
pub entitlements: Vec<Entitlement>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
pub struct IntegrationOwners {
#[serde(rename = "0")]
pub guild: Option<Snowflake>,
#[serde(rename = "1")]
pub user: Option<Snowflake>,
}
impl IntegrationOwners {
pub fn get(&self, integration_type: ApplicationIntegrationType) -> Option<Snowflake> {
match integration_type {
ApplicationIntegrationType::GuildInstall => self.guild,
ApplicationIntegrationType::UserInstall => self.user,
}
}
pub fn len(&self) -> usize {
usize::from(self.guild.is_some()) + usize::from(self.user.is_some())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl InteractionCommon {
pub fn invoking_user(&self) -> Option<&User> {
self.member
.as_ref()
.map(|member| &member.user)
.or(self.user.as_ref())
}
pub fn message(&self) -> Option<&Message> {
self.message.as_ref()?.get()
}
pub fn channel(&self) -> Option<&Channel> {
self.channel.as_ref()?.get()
}
}
#[derive(Debug, Clone)]
pub struct PingInteraction {
pub common: InteractionCommon,
}
#[derive(Debug, Clone)]
pub struct DataInteraction<D> {
pub common: InteractionCommon,
pub locale: Option<String>,
pub data: D,
}
impl<D: DeserializeOwned> DataInteraction<D> {
fn new<E: de::Error>(
common: InteractionCommon,
locale: Option<String>,
data: Option<Box<RawValue>>,
) -> Result<Self, E> {
let data = data.ok_or_else(|| E::missing_field("data"))?;
Ok(Self {
common,
locale,
data: serde_json::from_slice(data.get().as_bytes()).map_err(E::custom)?,
})
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ApplicationCommandInteractionData {
pub id: Snowflake,
pub name: String,
#[serde(rename = "type")]
pub t: ApplicationCommandType,
pub resolved: Option<ResolvedData>,
pub options: Option<OptionList>,
pub guild_id: Option<Snowflake>,
pub target_id: Option<Snowflake>,
}
impl ApplicationCommandInteractionData {
pub fn resolved_user(&self, snowflake: &Snowflake) -> Option<&User> {
self.resolved
.as_ref()
.and_then(|r| r.users.as_ref())
.and_then(|u| u.get(snowflake))
}
pub fn resolved_member(&self, snowflake: &Snowflake) -> Option<&PartialMember> {
self.resolved
.as_ref()
.and_then(|r| r.members.as_ref())
.and_then(|u| u.get(snowflake))
}
pub fn resolved_role(&self, snowflake: &Snowflake) -> Option<&Role> {
self.resolved
.as_ref()
.and_then(|r| r.roles.as_ref())
.and_then(|u| u.get(snowflake))
}
pub fn resolved_channel(&self, snowflake: &Snowflake) -> Option<&PartialChannel> {
self.resolved
.as_ref()
.and_then(|r| r.channels.as_ref())
.and_then(|c| c.get(snowflake))
}
pub fn resolved_message(&self, snowflake: &Snowflake) -> Option<&Message> {
self.resolved
.as_ref()
.and_then(|r| r.messages.as_ref())
.and_then(|m| m.get(snowflake))
}
pub fn resolved_attachment(&self, snowflake: &Snowflake) -> Option<&Attachment> {
self.resolved
.as_ref()
.and_then(|r| r.attachments.as_ref())
.and_then(|a| a.get(snowflake))
}
pub fn first_option(&self) -> Option<&ApplicationCommandInteractionDataOption> {
self.options.as_ref().and_then(|o| o.single())
}
pub fn focused_option(&self) -> Option<&FocusedOption> {
self.options.as_ref().and_then(|o| o.focused())
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct MessageComponentData {
pub custom_id: String,
pub component_type: MessageComponentType,
pub values: Option<Vec<String>>,
pub resolved: Option<ResolvedData>,
}
#[derive(Debug, Clone, Deserialize_repr)]
#[repr(u8)]
pub enum MessageComponentType {
ActionRow = 1,
Button = 2,
StringSelect = 3,
TextInput = 4,
UserSelect = 5,
RoleSelect = 6,
MentionableSelect = 7,
ChannelSelect = 8,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ModalSubmitData {
pub custom_id: String,
pub components: Vec<ModalSubmitComponent>,
pub resolved: Option<ResolvedData>,
}
impl ModalSubmitData {
pub fn value(&self, custom_id: &str) -> Option<&str> {
self.find(custom_id).and_then(ModalSubmitComponent::text)
}
pub fn checked(&self, custom_id: &str) -> Option<bool> {
self.find(custom_id).and_then(ModalSubmitComponent::checked)
}
pub fn resolved_user(&self, id: &Snowflake) -> Option<&User> {
self.resolved.as_ref()?.users.as_ref()?.get(id)
}
pub fn resolved_member(&self, id: &Snowflake) -> Option<&PartialMember> {
self.resolved.as_ref()?.members.as_ref()?.get(id)
}
pub fn resolved_role(&self, id: &Snowflake) -> Option<&Role> {
self.resolved.as_ref()?.roles.as_ref()?.get(id)
}
pub fn resolved_channel(&self, id: &Snowflake) -> Option<&PartialChannel> {
self.resolved.as_ref()?.channels.as_ref()?.get(id)
}
pub fn resolved_attachment(&self, id: &Snowflake) -> Option<&Attachment> {
self.resolved.as_ref()?.attachments.as_ref()?.get(id)
}
pub fn values(&self, custom_id: &str) -> Option<&[String]> {
self.find(custom_id).and_then(|c| c.values.as_deref())
}
pub fn find(&self, custom_id: &str) -> Option<&ModalSubmitComponent> {
self.components.iter().find_map(|c| c.find(custom_id))
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ModalSubmitComponent {
#[serde(rename = "type")]
pub t: u8,
pub custom_id: Option<String>,
pub value: Option<SubmittedValue>,
pub values: Option<Vec<String>>,
pub components: Option<Vec<ModalSubmitComponent>>,
pub component: Option<Box<ModalSubmitComponent>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub enum SubmittedValue {
Text(String),
Checked(bool),
}
impl ModalSubmitComponent {
pub fn text(&self) -> Option<&str> {
match &self.value {
Some(SubmittedValue::Text(text)) => Some(text),
_ => None,
}
}
pub fn checked(&self) -> Option<bool> {
match self.value {
Some(SubmittedValue::Checked(checked)) => Some(checked),
_ => None,
}
}
fn find(&self, custom_id: &str) -> Option<&ModalSubmitComponent> {
if self.custom_id.as_deref() == Some(custom_id) {
return Some(self);
}
self.components
.iter()
.flatten()
.chain(self.component.as_deref())
.find_map(|c| c.find(custom_id))
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ResolvedData {
pub users: Option<SnowflakeMap<User>>,
pub members: Option<SnowflakeMap<PartialMember>>,
pub roles: Option<SnowflakeMap<Role>>,
pub channels: Option<SnowflakeMap<PartialChannel>>,
pub messages: Option<SnowflakeMap<Message>>,
pub attachments: Option<SnowflakeMap<Attachment>>,
}
pub type StringOption = ValueOption<String>;
pub type IntegerOption = ValueOption<i64>;
pub type BooleanOption = ValueOption<bool>;
pub type SnowflakeOption = ValueOption<Snowflake>;
pub type NumberOption = ValueOption<f64>;
#[derive(Debug, Clone)]
pub enum ApplicationCommandInteractionDataOption {
Subcommand(Subcommand),
SubcommandGroup(SubcommandGroup),
String(StringOption),
Integer(IntegerOption),
Boolean(BooleanOption),
User(SnowflakeOption),
Channel(SnowflakeOption),
Role(SnowflakeOption),
Mentionable(SnowflakeOption),
Number(NumberOption),
Attachment(SnowflakeOption),
Focused(FocusedOption),
}
#[derive(Debug, Clone)]
pub struct FocusedOption {
pub name: String,
pub t: u8,
pub value: String,
}
impl ApplicationCommandInteractionDataOption {
pub fn name(&self) -> &str {
match self {
Self::Subcommand(s) => &s.name,
Self::SubcommandGroup(s) => &s.name,
Self::String(s) => &s.name,
Self::Integer(s) => &s.name,
Self::Boolean(s) => &s.name,
Self::User(s) | Self::Channel(s) | Self::Role(s) | Self::Mentionable(s) => &s.name,
Self::Number(s) => &s.name,
Self::Attachment(s) => &s.name,
Self::Focused(s) => &s.name,
}
}
}
impl<'de> Deserialize<'de> for ApplicationCommandInteractionDataOption {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use ApplicationCommandInteractionDataOption as O;
use RawOptionValue as V;
let RawOption {
name,
t,
value,
options,
focused,
} = RawOption::deserialize(deserializer)?;
if focused == Some(true) && t > 2 {
let value = match value {
Some(V::String(s)) => s,
Some(V::Integer(i)) => i.to_string(),
Some(V::Number(n)) => n.to_string(),
Some(V::Boolean(b)) => b.to_string(),
None => String::new(),
};
return Ok(O::Focused(FocusedOption { name, t, value }));
}
Ok(match (t, value) {
(1, _) => O::Subcommand(Subcommand {
name,
options: OptionList(options.unwrap_or_default()),
focused,
}),
(2, _) => match options.and_then(|o| o.into_iter().next()) {
Some(O::Subcommand(subcommand)) => O::SubcommandGroup(SubcommandGroup {
name,
subcommand,
focused,
}),
_ => return Err(de::Error::custom("subcommand group without a subcommand")),
},
(3, Some(V::String(s))) => O::String(ValueOption::new(name, s, focused)),
(4, Some(V::Integer(i))) => O::Integer(ValueOption::new(name, i, focused)),
(5, Some(V::Boolean(b))) => O::Boolean(ValueOption::new(name, b, focused)),
(10, Some(V::Number(n))) => O::Number(ValueOption::new(name, n, focused)),
(10, Some(V::Integer(i))) => O::Number(ValueOption::new(name, i as f64, focused)),
(6..=9 | 11, Some(V::String(s))) => {
let Ok(id) = s.parse() else {
return Err(de::Error::custom(format_args!(
"option `{name}`: invalid id"
)));
};
let option = ValueOption::new(name, id, focused);
match t {
6 => O::User(option),
7 => O::Channel(option),
8 => O::Role(option),
9 => O::Mentionable(option),
_ => O::Attachment(option),
}
}
(3..=11, _) => {
return Err(de::Error::custom(format_args!(
"option `{name}`: value does not match type {t}"
)));
}
_ => return Err(de::Error::custom("unknown option type")),
})
}
}
#[derive(Deserialize)]
struct RawOption {
name: String,
#[serde(rename = "type")]
t: u8,
value: Option<RawOptionValue>,
options: Option<Vec<ApplicationCommandInteractionDataOption>>,
focused: Option<bool>,
}
enum RawOptionValue {
String(String),
Integer(i64),
Number(f64),
Boolean(bool),
}
impl<'de> Deserialize<'de> for RawOptionValue {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl de::Visitor<'_> for Visitor {
type Value = RawOptionValue;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a string, number, or boolean")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(RawOptionValue::String(v.to_string()))
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
Ok(RawOptionValue::String(v))
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
Ok(RawOptionValue::Integer(v))
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
i64::try_from(v)
.map(RawOptionValue::Integer)
.map_err(|_| E::invalid_value(de::Unexpected::Unsigned(v), &self))
}
fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
Ok(RawOptionValue::Number(v))
}
fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
Ok(RawOptionValue::Boolean(v))
}
}
deserializer.deserialize_any(Visitor)
}
}
#[derive(Debug, Clone, Default)]
pub struct OptionList(Vec<ApplicationCommandInteractionDataOption>);
impl OptionList {
pub const fn empty() -> Self {
OptionList(Vec::new())
}
pub fn iter(&self) -> impl Iterator<Item = &ApplicationCommandInteractionDataOption> {
self.0.iter()
}
pub fn focused(&self) -> Option<&FocusedOption> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::Focused(f) => Some(f),
ApplicationCommandInteractionDataOption::Subcommand(s) => s.options.focused(),
ApplicationCommandInteractionDataOption::SubcommandGroup(g) => {
g.subcommand.options.focused()
}
_ => None,
})
}
pub fn get_number_option(&self, name: &str) -> Option<&NumberOption> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::Number(s) if s.name == name => Some(s),
_ => None,
})
}
pub fn get_attachment_option(&self, name: &str) -> Option<&SnowflakeOption> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::Attachment(s) if s.name == name => Some(s),
_ => None,
})
}
pub fn single(&self) -> Option<&ApplicationCommandInteractionDataOption> {
self.0.first()
}
pub fn subcommand(&self) -> Option<&Subcommand> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::Subcommand(s) => Some(s),
_ => None,
})
}
pub fn subcommand_group(&self) -> Option<&SubcommandGroup> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::SubcommandGroup(s) => Some(s),
_ => None,
})
}
pub fn get_option(&self, name: &str) -> Option<&ApplicationCommandInteractionDataOption> {
self.0.iter().find(|o| o.name() == name)
}
pub fn get_string_option(&self, name: &str) -> Option<&StringOption> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::String(s) if s.name == name => Some(s),
_ => None,
})
}
pub fn get_integer_option(&self, name: &str) -> Option<&IntegerOption> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::Integer(s) if s.name == name => Some(s),
_ => None,
})
}
pub fn get_boolean_option(&self, name: &str) -> Option<&BooleanOption> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::Boolean(s) if s.name == name => Some(s),
_ => None,
})
}
pub fn get_user_option(&self, name: &str) -> Option<&SnowflakeOption> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::User(s) if s.name == name => Some(s),
_ => None,
})
}
pub fn get_channel_option(&self, name: &str) -> Option<&SnowflakeOption> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::Channel(s) if s.name == name => Some(s),
_ => None,
})
}
pub fn get_role_option(&self, name: &str) -> Option<&SnowflakeOption> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::Role(s) if s.name == name => Some(s),
_ => None,
})
}
pub fn get_mentionable_option(&self, name: &str) -> Option<&SnowflakeOption> {
self.0.iter().find_map(|o| match o {
ApplicationCommandInteractionDataOption::Mentionable(s) if s.name == name => Some(s),
_ => None,
})
}
}
impl<'de> Deserialize<'de> for OptionList {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(OptionList(
Vec::<ApplicationCommandInteractionDataOption>::deserialize(deserializer)?,
))
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Subcommand {
pub name: String,
#[serde(default)]
pub options: OptionList,
pub focused: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct SubcommandGroup {
pub name: String,
pub subcommand: Subcommand,
pub focused: Option<bool>,
}
impl<'de> Deserialize<'de> for SubcommandGroup {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct InnerData {
name: String,
options: Vec<ApplicationCommandInteractionDataOption>,
focused: Option<bool>,
}
let InnerData {
name,
focused,
options,
} = InnerData::deserialize(deserializer)?;
match options.into_iter().next() {
Some(ApplicationCommandInteractionDataOption::Subcommand(subcommand)) => {
Ok(SubcommandGroup {
name,
subcommand,
focused,
})
}
_ => Err(serde::de::Error::custom("Not a subcommand")),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ValueOption<T> {
pub name: String,
pub value: T,
pub focused: Option<bool>,
}
impl<T> ValueOption<T> {
fn new(name: String, value: T, focused: Option<bool>) -> Self {
Self {
name,
value,
focused,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize_repr, Serialize_repr)]
#[repr(u8)]
pub enum ApplicationCommandType {
ChatInput = 1,
User = 2,
Message = 3,
}
#[derive(Debug, Clone, Deserialize)]
pub struct InteractionData {
pub id: Snowflake,
pub name: String,
#[serde(rename = "type")]
pub t: ApplicationCommandType,
pub resolved: Option<ResolvedData>,
pub options: Option<Vec<ApplicationCommandInteractionDataOption>>,
pub guild_id: Option<Snowflake>,
pub target_id: Option<Snowflake>,
}
#[cfg(test)]
mod tests {
use super::*;
fn button_click(message: serde_json::Value) -> MessageComponentInteraction {
let json = serde_json::json!({
"id": "1", "application_id": "2", "type": 3, "token": "t", "version": 1,
"channel": { "id": "3", "type": 0, "name": "general" },
"data": { "custom_id": "go", "component_type": 2 },
"message": message,
});
match serde_json::from_value(json).unwrap() {
Interaction::MessageComponent(click) => click,
_ => unreachable!(),
}
}
#[test]
pub fn message_and_channel_parse_on_first_read() {
let click = button_click(serde_json::json!({
"id": "4", "channel_id": "3", "content": "hello", "timestamp": "2026-01-01T00:00:00Z",
"edited_timestamp": null, "tts": false, "mention_everyone": false, "mentions": [],
"mention_roles": [], "attachments": [], "embeds": [], "pinned": false, "type": 0,
"author": { "id": "5", "username": "a", "discriminator": "0", "avatar": null },
"components": [{ "type": 1, "components": [
{ "type": 2, "style": 1, "label": "Go", "custom_id": "go" },
] }],
}));
let message = click.common.message().unwrap();
assert_eq!(message.content, "hello");
assert!(matches!(
message.components.as_deref(),
Some([crate::types::Component::ActionRow(_)])
));
assert!(std::ptr::eq(message, click.common.message().unwrap()));
assert_eq!(
click.common.channel().unwrap().name.as_deref(),
Some("general")
);
}
#[test]
pub fn malformed_message_does_not_fail_the_interaction() {
let click = button_click(serde_json::json!({ "id": "not a message" }));
assert_eq!(click.data.custom_id, "go");
assert!(click.common.message().is_none());
assert!(click.common.message.as_ref().unwrap().parse().is_err());
}
#[test]
pub fn ping_interaction() {
let json = r#"{
"application_id": "1052322265397739523",
"type": 1,
"token": "A_UNIQUE_TOKEN",
"member": {
"user": {
"id": "53908232506183680",
"username": "Mason",
"avatar": "a_d5efa99b3eeaa7dd43acca82f5692432",
"discriminator": "1337",
"public_flags": 131141
},
"roles": ["539082325061836999"],
"premium_since": null,
"permissions": "2147483647",
"pending": false,
"nick": null,
"mute": false,
"joined_at": "2017-03-13T19:19:14.040000+00:00",
"is_pending": false,
"deaf": false,
"flags": 0
},
"id": "786008729715212338",
"guild_id": "290926798626357999",
"app_permissions": "442368",
"guild_locale": "en-US",
"locale": "en-US",
"channel_id": "645027906669510667",
"version": 1
}"#;
let res = serde_json::from_str::<Interaction>(json);
assert!(res.is_ok());
let interaction = res.unwrap();
assert!(matches!(
interaction,
Interaction::Ping(PingInteraction { .. })
))
}
#[test]
pub fn command_interaction() {
let json = r#"{
"application_id": "1052322265397739523",
"version": 1,
"type": 2,
"token": "A_UNIQUE_TOKEN",
"member": {
"user": {
"id": "53908232506183680",
"username": "Mason",
"avatar": "a_d5efa99b3eeaa7dd43acca82f5692432",
"discriminator": "1337",
"public_flags": 131141
},
"roles": ["539082325061836999"],
"premium_since": null,
"permissions": "2147483647",
"pending": false,
"nick": null,
"mute": false,
"joined_at": "2017-03-13T19:19:14.040000+00:00",
"is_pending": false,
"deaf": false,
"flags": 0
},
"id": "786008729715212338",
"guild_id": "290926798626357999",
"app_permissions": "442368",
"guild_locale": "en-US",
"locale": "en-US",
"data": {
"options": [{
"type": 3,
"name": "cardname",
"value": "The Gitrog Monster"
}],
"type": 1,
"name": "cardsearch",
"id": "771825006014889984"
},
"channel_id": "645027906669510667"
}"#;
let res = serde_json::from_str::<Interaction>(json);
assert!(res.is_ok());
let interaction = res.unwrap();
assert!(matches!(
interaction,
Interaction::ApplicationCommand(DataInteraction { .. })
))
}
#[test]
pub fn real_interaction() {
let json = r#"{
"app_permissions": "137411140374081",
"application_id": "1052322265397739523",
"channel": {
"flags": 0,
"guild_id": "798662131062931547",
"id": "941169456686723122",
"last_message_id": "1100155827400229026",
"name": "bot-stuff",
"nsfw": false,
"parent_id": "798662131678969866",
"permissions": "140737488355327",
"position": 1,
"rate_limit_per_user": 0,
"topic": null,
"type": 0
},
"channel_id": "941169456686723122",
"data": {
"guild_id": "798662131062931547",
"id": "1052358444704862218",
"name": "ping",
"type": 1
},
"entitlement_sku_ids": [],
"entitlements": [],
"guild_id": "798662131062931547",
"guild_locale": "en-US",
"id": "1100173248714518568",
"locale": "en-US",
"member": {
"avatar": null,
"communication_disabled_until": null,
"deaf": false,
"flags": 0,
"is_pending": false,
"joined_at": "2021-01-12T21:18:10.481000+00:00",
"mute": false,
"nick": null,
"pending": false,
"permissions": "140737488355327",
"premium_since": null,
"roles": [
"943607715639484456"
],
"user": {
"avatar": "fa82e15e24ee16c9fcbf8dd34d10b4cc",
"avatar_decoration": null,
"discriminator": "9846",
"display_name": null,
"global_name": null,
"id": "282265607313817601",
"public_flags": 0,
"username": "BlueFrog"
}
},
"token": "aW50ZXJhY3Rpb246MTEwMDE3MzI0ODcxNDUxODU2ODppVTFuSkNSbndrZ01Na3RCWk81MVhTWkdSbk8yTlBaM1U3Z3JlckR4YUZJMTZFTm9wc21nZnlaSnN4ZUZCTTd0Q0Jzc09ac3BHV1E1MGlBZGZnZzh0NDJmTElIcTB1M0FZQTJPS1BxcG1GTEtZUjNDWWFEamhEeTRPMWZnS0R4dQ",
"type": 2,
"version": 1
}"#;
let interaction = serde_json::from_str::<Interaction>(json);
println!("{:#?}", interaction);
assert!(interaction.is_ok());
}
#[test]
pub fn current_format_interaction() {
let json = r#"{
"app_permissions": "2248473465835073",
"application_id": "1052322265397739523",
"attachment_size_limit": 10485760,
"authorizing_integration_owners": { "0": "798662131062931547" },
"channel": {
"flags": 0,
"guild_id": "798662131062931547",
"id": "941169456686723122",
"last_message_id": "1100155827400229026",
"name": "bot-stuff",
"nsfw": false,
"parent_id": "798662131678969866",
"permissions": "2251799813685247",
"position": 1,
"rate_limit_per_user": 0,
"topic": null,
"type": 0
},
"channel_id": "941169456686723122",
"context": 0,
"data": {
"guild_id": "798662131062931547",
"id": "1052358444704862218",
"name": "ping",
"type": 1
},
"entitlement_sku_ids": [],
"entitlements": [],
"guild": { "features": [], "id": "798662131062931547", "locale": "en-US" },
"guild_id": "798662131062931547",
"guild_locale": "en-US",
"id": "1100173248714518568",
"locale": "en-US",
"member": {
"avatar": null,
"banner": null,
"collectibles": null,
"communication_disabled_until": null,
"deaf": false,
"flags": 0,
"joined_at": "2021-01-12T21:18:10.481000+00:00",
"mute": false,
"nick": null,
"pending": false,
"permissions": "2251799813685247",
"premium_since": null,
"roles": ["943607715639484456"],
"unusual_dm_activity_until": null,
"user": {
"avatar": "fa82e15e24ee16c9fcbf8dd34d10b4cc",
"avatar_decoration_data": null,
"clan": null,
"collectibles": null,
"discriminator": "0",
"global_name": "Blue Frog",
"id": "282265607313817601",
"primary_guild": null,
"public_flags": 0,
"username": "bluefrog"
}
},
"token": "A_UNIQUE_TOKEN",
"type": 2,
"version": 1
}"#;
let Interaction::ApplicationCommand(interaction) =
serde_json::from_str::<Interaction>(json).unwrap()
else {
panic!("expected application command");
};
let member = interaction.common.member.unwrap();
assert_eq!(member.user.discriminator, "0");
assert_eq!(member.user.global_name.as_deref(), Some("Blue Frog"));
assert_eq!(member.display_name(), "Blue Frog");
}
fn user_install_interaction(context: u8, location: &str) -> ApplicationCommandInteraction {
let json = format!(
r#"{{
"app_permissions": "562949953601536",
"application_id": "1052322265397739523",
"attachment_size_limit": 10485760,
"authorizing_integration_owners": {{ "1": "282265607313817601" }},
"context": {context},
"data": {{ "id": "1052358444704862218", "name": "ping", "type": 1 }},
"entitlements": [],
"id": "1100173248714518568",
"locale": "en-US",
{location},
"token": "A_UNIQUE_TOKEN",
"type": 2,
"version": 1
}}"#
);
match serde_json::from_str::<Interaction>(&json).unwrap() {
Interaction::ApplicationCommand(interaction) => interaction,
other => panic!("expected application command, got {other:?}"),
}
}
const USER: &str = r#"{
"avatar": null,
"discriminator": "0",
"global_name": "Blue Frog",
"id": "282265607313817601",
"public_flags": 0,
"username": "bluefrog"
}"#;
#[test]
pub fn user_install_guild_context() {
let location = format!(
r#""guild_id": "798662131062931547",
"guild_locale": "en-US",
"channel_id": "941169456686723122",
"member": {{
"avatar": null,
"deaf": false,
"flags": 0,
"joined_at": "2021-01-12T21:18:10.481000+00:00",
"mute": false,
"nick": "Froggy",
"pending": false,
"permissions": "2251799813685247",
"premium_since": null,
"roles": [],
"user": {USER}
}}"#
);
let interaction = user_install_interaction(0, &location);
let common = &interaction.common;
assert_eq!(common.context, Some(InteractionContextType::Guild));
assert_eq!(
common
.authorizing_integration_owners
.get(ApplicationIntegrationType::UserInstall)
.unwrap(),
Snowflake::from_u64(282265607313817601)
);
assert!(common.user.is_none());
assert_eq!(
common.invoking_user().unwrap().id,
Snowflake::from_u64(282265607313817601)
);
}
#[test]
pub fn bot_dm_context() {
let location = format!(
r#""channel": {{
"flags": 0,
"id": "1100155827400229026",
"last_message_id": null,
"recipients": [{USER}],
"type": 1
}},
"channel_id": "1100155827400229026",
"user": {USER}"#
);
let interaction = user_install_interaction(1, &location);
let common = &interaction.common;
assert_eq!(common.context, Some(InteractionContextType::BotDm));
assert!(common.member.is_none());
assert_eq!(common.invoking_user().unwrap().username, "bluefrog");
}
#[test]
pub fn private_channel_context() {
let location = format!(
r#""channel": {{ "id": "1100155827400229026", "type": 3 }},
"channel_id": "1100155827400229026",
"user": {USER}"#
);
let interaction = user_install_interaction(2, &location);
let common = &interaction.common;
assert_eq!(common.context, Some(InteractionContextType::PrivateChannel));
assert!(common.guild_id.is_none());
assert!(common.member.is_none());
assert_eq!(common.invoking_user().unwrap().display_name(), "Blue Frog");
}
#[test]
pub fn guild_install_owners() {
let json = r#"{
"application_id": "1052322265397739523",
"authorizing_integration_owners": { "0": "798662131062931547", "1": "282265607313817601" },
"context": 0,
"id": "1100173248714518568",
"token": "A_UNIQUE_TOKEN",
"type": 1,
"version": 1
}"#;
let Interaction::Ping(ping) = serde_json::from_str::<Interaction>(json).unwrap() else {
panic!("expected ping");
};
let owners = &ping.common.authorizing_integration_owners;
assert_eq!(
owners
.get(ApplicationIntegrationType::GuildInstall)
.unwrap(),
Snowflake::from_u64(798662131062931547)
);
assert_eq!(
owners.get(ApplicationIntegrationType::UserInstall).unwrap(),
Snowflake::from_u64(282265607313817601)
);
assert!(ping.common.invoking_user().is_none());
}
}