use std::collections::HashMap;
use serde::{Serialize, ser::SerializeMap};
use crate::types::{
AllowedMentions, Component, Container, Embed, File, MessageFlags, PartialAttachment, PollCreate,
};
const TYPE_KEY: &str = "type";
const DATA_KEY: &str = "data";
#[derive(Debug, Clone)]
pub enum InteractionResponse {
Pong,
ChannelMessageWithSource(MessageCallbackData),
DeferredChannelMessageWithSource(Option<MessageFlags>),
DeferredUpdateMessage,
UpdateMessage(MessageCallbackData),
ApplicationCommandAutocompleteResult(AutocompleteCallbackData),
Modal(ModalCallbackData),
}
impl InteractionResponse {
pub fn respond_with_message(content: String) -> Self {
InteractionResponse::ChannelMessageWithSource(MessageCallbackData {
content: Some(content),
..Default::default()
})
}
pub fn respond_with_ephemeral_message(content: String) -> Self {
InteractionResponse::ChannelMessageWithSource(MessageCallbackData {
content: Some(content),
flags: Some(MessageFlags::Ephemeral),
..Default::default()
})
}
pub fn respond_with_embed(embed: Embed) -> Self {
InteractionResponse::ChannelMessageWithSource(MessageCallbackData {
embeds: Some(vec![embed]),
..Default::default()
})
}
pub fn respond_with_components(components: Vec<Component>) -> Self {
InteractionResponse::ChannelMessageWithSource(MessageCallbackData::components_v2(
components,
))
}
pub fn message_data(&self) -> Option<&MessageCallbackData> {
match self {
InteractionResponse::ChannelMessageWithSource(data)
| InteractionResponse::UpdateMessage(data) => Some(data),
_ => None,
}
}
pub fn message_data_mut(&mut self) -> Option<&mut MessageCallbackData> {
match self {
InteractionResponse::ChannelMessageWithSource(data)
| InteractionResponse::UpdateMessage(data) => Some(data),
_ => None,
}
}
pub fn into_message_data(self) -> Option<MessageCallbackData> {
match self {
InteractionResponse::ChannelMessageWithSource(data)
| InteractionResponse::UpdateMessage(data) => Some(data),
_ => None,
}
}
pub fn files(&self) -> &[File] {
self.message_data().map_or(&[], |data| &data.files)
}
pub fn with_file(mut self, file: File) -> Self {
if let Some(data) = self.message_data_mut() {
data.files.push(file);
}
self
}
pub fn respond_with_autocomplete_choices(choices: Vec<ApplicationCommandOptionChoice>) -> Self {
InteractionResponse::ApplicationCommandAutocompleteResult(AutocompleteCallbackData {
choices,
})
}
}
impl Serialize for InteractionResponse {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(2))?;
match self {
InteractionResponse::Pong => {
map.serialize_entry(TYPE_KEY, &1)?;
}
InteractionResponse::ChannelMessageWithSource(data) => {
map.serialize_entry(TYPE_KEY, &4)?;
map.serialize_entry(DATA_KEY, &data)?;
}
InteractionResponse::DeferredChannelMessageWithSource(flags) => {
map.serialize_entry(TYPE_KEY, &5)?;
if let Some(flags) = flags {
map.serialize_entry(DATA_KEY, &DeferredData { flags })?;
}
}
InteractionResponse::DeferredUpdateMessage => {
map.serialize_entry(TYPE_KEY, &6)?;
}
InteractionResponse::UpdateMessage(data) => {
map.serialize_entry(TYPE_KEY, &7)?;
map.serialize_entry(DATA_KEY, &data)?;
}
InteractionResponse::ApplicationCommandAutocompleteResult(data) => {
map.serialize_entry(TYPE_KEY, &8)?;
map.serialize_entry(DATA_KEY, &data)?;
}
InteractionResponse::Modal(data) => {
map.serialize_entry(TYPE_KEY, &9)?;
map.serialize_entry(DATA_KEY, &data)?;
}
};
map.end()
}
}
#[derive(Serialize)]
struct DeferredData<'a> {
flags: &'a MessageFlags,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct MessageCallbackData {
#[serde(skip_serializing_if = "Option::is_none")]
pub tts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_mentions: Option<AllowedMentions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<MessageFlags>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Vec<Component>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<PartialAttachment>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub poll: Option<PollCreate>,
#[serde(skip)]
pub files: Vec<File>,
#[serde(skip)]
pub unthemed: bool,
}
impl MessageCallbackData {
pub fn components_v2(components: Vec<Component>) -> Self {
MessageCallbackData {
components: Some(components),
flags: Some(MessageFlags::IsComponentsV2),
..Default::default()
}
}
pub fn is_components_v2(&self) -> bool {
self.flags
.is_some_and(|flags| flags.contains(MessageFlags::IsComponentsV2))
}
pub fn containers_mut(&mut self) -> impl Iterator<Item = &mut Container> {
self.components
.iter_mut()
.flatten()
.filter_map(|component| match component {
Component::Container(container) => Some(container),
_ => None,
})
}
}
#[derive(Debug, Clone, Serialize)]
pub struct AutocompleteCallbackData {
pub choices: Vec<ApplicationCommandOptionChoice>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ApplicationCommandOptionChoice {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name_localizations: Option<HashMap<String, String>>,
pub value: ApplicationCommandOptionChoiceValue,
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum ApplicationCommandOptionChoiceValue {
String(String),
Integer(i64),
Double(f64),
}
#[derive(Debug, Clone, Serialize)]
pub struct ModalCallbackData {
pub custom_id: String,
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Vec<Component>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_mentions: Option<AllowedMentions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<MessageFlags>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn serialize_test() {
let response = InteractionResponse::ChannelMessageWithSource(MessageCallbackData {
content: Some(String::from("hello")),
..Default::default()
});
println!("{}", serde_json::to_string_pretty(&response).unwrap());
}
#[test]
pub fn serialize_deferred() {
assert_eq!(
serde_json::to_value(InteractionResponse::DeferredChannelMessageWithSource(None))
.unwrap(),
serde_json::json!({ "type": 5 })
);
assert_eq!(
serde_json::to_value(InteractionResponse::DeferredChannelMessageWithSource(Some(
MessageFlags::Ephemeral
)))
.unwrap(),
serde_json::json!({ "type": 5, "data": { "flags": 64 } })
);
}
#[test]
pub fn serialize_components_v2() {
let container = Container::new(vec![Component::text("hi")])
.with_accent_color(0xff0000)
.spoiler();
let response = InteractionResponse::respond_with_components(vec![container.into()]);
let json = serde_json::json!({ "type": 4, "data": {
"flags": 1 << 15,
"components": [{
"type": 17,
"components": [{ "type": 10, "content": "hi" }],
"accent_color": 0xff0000,
"spoiler": true,
}],
}});
assert_eq!(serde_json::to_value(&response).unwrap(), json);
let parsed: Component = serde_json::from_value(json["data"]["components"][0].clone())
.expect("container deserializes");
let Component::Container(container) = parsed else {
panic!("not a container");
};
assert_eq!(container.accent_color, Some(0xff0000));
assert!(matches!(container.components[0], Component::TextDisplay(_)));
}
#[test]
pub fn serialize_ephemeral_message() {
let response = InteractionResponse::respond_with_ephemeral_message(String::from("hello"));
assert_eq!(
serde_json::to_value(&response).unwrap(),
serde_json::json!({ "type": 4, "data": { "content": "hello", "flags": 64 } })
);
}
}