titanium_model/
reaction.rs

1//! Reaction types for Discord message reactions.
2//!
3//! Reactions are emoji added to messages by users.
4
5use crate::Snowflake;
6use crate::TitanString;
7use serde::{Deserialize, Serialize};
8
9/// Event data for `MESSAGE_REACTION_ADD`.
10#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct MessageReactionAddEvent<'a> {
12    /// ID of the user.
13    pub user_id: Snowflake,
14
15    /// ID of the channel.
16    pub channel_id: Snowflake,
17
18    /// ID of the message.
19    pub message_id: Snowflake,
20
21    /// ID of the guild.
22    #[serde(default)]
23    pub guild_id: Option<Snowflake>,
24
25    /// Member who reacted if in a guild.
26    #[serde(default)]
27    pub member: Option<super::member::GuildMember<'a>>,
28
29    /// The emoji used to react.
30    pub emoji: ReactionEmoji<'a>,
31
32    /// ID of the user who authored the message.
33    #[serde(default)]
34    pub message_author_id: Option<Snowflake>,
35
36    /// Whether this is a super-reaction.
37    #[serde(default)]
38    pub burst: bool,
39
40    /// Colors used for super-reaction animation (hex format).
41    #[serde(default)]
42    pub burst_colors: Vec<String>,
43
44    /// The type of reaction.
45    #[serde(default, rename = "type")]
46    pub reaction_type: u8,
47}
48
49/// Event data for `MESSAGE_REACTION_REMOVE`.
50#[derive(Debug, Clone, Deserialize, Serialize)]
51pub struct MessageReactionRemoveEvent<'a> {
52    /// ID of the user.
53    pub user_id: Snowflake,
54
55    /// ID of the channel.
56    pub channel_id: Snowflake,
57
58    /// ID of the message.
59    pub message_id: Snowflake,
60
61    /// ID of the guild.
62    #[serde(default)]
63    pub guild_id: Option<Snowflake>,
64
65    /// The emoji used to react.
66    pub emoji: ReactionEmoji<'a>,
67
68    /// Whether this was a super-reaction.
69    #[serde(default)]
70    pub burst: bool,
71
72    /// The type of reaction.
73    #[serde(default, rename = "type")]
74    pub reaction_type: u8,
75}
76
77/// Event data for `MESSAGE_REACTION_REMOVE_ALL`.
78#[derive(Debug, Clone, Deserialize, Serialize)]
79pub struct MessageReactionRemoveAllEvent {
80    /// ID of the channel.
81    pub channel_id: Snowflake,
82
83    /// ID of the message.
84    pub message_id: Snowflake,
85
86    /// ID of the guild.
87    #[serde(default)]
88    pub guild_id: Option<Snowflake>,
89}
90
91/// Event data for `MESSAGE_REACTION_REMOVE_EMOJI`.
92#[derive(Debug, Clone, Deserialize, Serialize)]
93pub struct MessageReactionRemoveEmojiEvent<'a> {
94    /// ID of the channel.
95    pub channel_id: Snowflake,
96
97    /// ID of the message.
98    pub message_id: Snowflake,
99
100    /// ID of the guild.
101    #[serde(default)]
102    pub guild_id: Option<Snowflake>,
103
104    /// The emoji that was removed.
105    pub emoji: ReactionEmoji<'a>,
106}
107
108/// Emoji used in a reaction.
109#[derive(Debug, Clone, Deserialize, Serialize, Default)]
110pub struct ReactionEmoji<'a> {
111    /// Emoji ID (null for standard emoji).
112    #[serde(default)]
113    pub id: Option<Snowflake>,
114
115    /// Emoji name.
116    #[serde(default)]
117    pub name: Option<TitanString<'a>>,
118
119    /// Whether this emoji is animated.
120    #[serde(default)]
121    pub animated: bool,
122}
123
124impl<'a> ReactionEmoji<'a> {
125    /// Create a unicode emoji reaction (e.g., "👍").
126    #[inline]
127    pub fn unicode(name: impl Into<TitanString<'a>>) -> Self {
128        Self {
129            id: None,
130            name: Some(name.into()),
131            animated: false,
132        }
133    }
134
135    /// Create a custom emoji reaction.
136    #[inline]
137    pub fn custom(id: impl Into<Snowflake>, name: impl Into<TitanString<'a>>) -> Self {
138        Self {
139            id: Some(id.into()),
140            name: Some(name.into()),
141            animated: false,
142        }
143    }
144
145    /// Create an animated custom emoji reaction.
146    #[inline]
147    pub fn animated(id: impl Into<Snowflake>, name: impl Into<TitanString<'a>>) -> Self {
148        Self {
149            id: Some(id.into()),
150            name: Some(name.into()),
151            animated: true,
152        }
153    }
154
155    /// Format this emoji for use in Discord text (e.g., "<:name:id>").
156    #[must_use]
157    pub fn format(&self) -> String {
158        match (&self.id, &self.name) {
159            (Some(id), Some(name)) if self.animated => format!("<a:{}:{}>", name, id.0),
160            (Some(id), Some(name)) => format!("<:{}:{}>", name, id.0),
161            (None, Some(name)) => name.to_string(),
162            _ => String::new(),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_reaction_add_event() {
173        let json = r#"{
174            "user_id": "123",
175            "channel_id": "456",
176            "message_id": "789",
177            "emoji": {"name": "👍"}
178        }"#;
179
180        let event: MessageReactionAddEvent = crate::json::from_str(json).unwrap();
181        assert_eq!(event.emoji.name, Some(TitanString::Borrowed("👍")));
182    }
183
184    #[test]
185    fn test_custom_emoji() {
186        let json = r#"{
187            "id": "123456789",
188            "name": "custom_emoji",
189            "animated": true
190        }"#;
191
192        let emoji: ReactionEmoji = crate::json::from_str(json).unwrap();
193        assert!(emoji.animated);
194        assert!(emoji.id.is_some());
195    }
196}