Skip to main content

kick_api/models/
live_chat.rs

1use serde::Deserialize;
2
3/// Pusher wire-format message (outer envelope)
4#[derive(Debug, Clone, Deserialize)]
5pub(crate) struct PusherMessage {
6    pub event: String,
7    pub data: String,
8    #[serde(default)]
9    pub channel: Option<String>,
10}
11
12/// A raw Pusher event received from the WebSocket.
13///
14/// Useful for debugging or handling event types beyond chat messages.
15#[derive(Debug, Clone)]
16pub struct PusherEvent {
17    /// The Pusher event name (e.g. `App\Events\ChatMessageEvent`)
18    pub event: String,
19    /// The channel this event was received on, if any
20    pub channel: Option<String>,
21    /// The raw JSON data payload (may need a second parse — Pusher double-encodes)
22    pub data: String,
23}
24
25/// A live chat message received over the Pusher WebSocket
26#[derive(Debug, Clone, Deserialize)]
27pub struct LiveChatMessage {
28    /// Unique message identifier
29    pub id: String,
30
31    /// The chatroom this message was sent in (may not be present in all payloads)
32    #[serde(default)]
33    pub chatroom_id: Option<u64>,
34
35    /// Message text content
36    pub content: String,
37
38    /// Message type (e.g. "message" or "reply")
39    #[serde(rename = "type")]
40    pub r#type: String,
41
42    /// ISO 8601 timestamp of when the message was created
43    #[serde(default)]
44    pub created_at: Option<String>,
45
46    /// The user who sent this message
47    pub sender: ChatSender,
48
49    /// Reply metadata, present when this message is a reply
50    #[serde(default)]
51    pub metadata: Option<ChatMessageMetadata>,
52}
53
54/// Metadata attached to a reply message
55#[derive(Debug, Clone, Deserialize)]
56pub struct ChatMessageMetadata {
57    /// The original message being replied to
58    #[serde(default)]
59    pub original_sender: Option<OriginalSender>,
60
61    /// The original message content
62    #[serde(default)]
63    pub original_message: Option<OriginalMessage>,
64}
65
66/// The sender of the message being replied to
67#[derive(Debug, Clone, Deserialize)]
68pub struct OriginalSender {
69    pub username: String,
70}
71
72/// The content of the message being replied to
73#[derive(Debug, Clone, Deserialize)]
74pub struct OriginalMessage {
75    pub content: String,
76}
77
78/// Sender information for a live chat message
79#[derive(Debug, Clone, Deserialize)]
80pub struct ChatSender {
81    /// Unique user identifier
82    pub id: u64,
83
84    /// Display username
85    pub username: String,
86
87    /// URL-friendly username slug
88    #[serde(default)]
89    pub slug: Option<String>,
90
91    /// Visual identity (color, badges)
92    pub identity: ChatIdentity,
93}
94
95/// Visual identity information for a chat sender
96#[derive(Debug, Clone, Deserialize)]
97pub struct ChatIdentity {
98    /// Username color hex code
99    pub color: String,
100
101    /// List of badges the user has
102    pub badges: Vec<ChatBadge>,
103}
104
105/// A badge displayed next to a user's name in chat
106#[derive(Debug, Clone, Deserialize)]
107pub struct ChatBadge {
108    /// Badge type identifier
109    #[serde(rename = "type")]
110    pub r#type: String,
111
112    /// Badge display text
113    pub text: String,
114
115    /// Optional count (e.g. subscription months)
116    #[serde(default)]
117    pub count: Option<u32>,
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_deserialize_chat_message() {
126        let json = r##"{
127            "id": "abc-123",
128            "chatroom_id": 12345,
129            "content": "hello world",
130            "type": "message",
131            "created_at": "2025-01-01T00:00:00Z",
132            "sender": {
133                "id": 9999,
134                "username": "hello_kiko",
135                "slug": "hello_kiko",
136                "identity": {
137                    "color": "#FF0000",
138                    "badges": []
139                }
140            }
141        }"##;
142
143        let msg: LiveChatMessage = serde_json::from_str(json).unwrap();
144        assert_eq!(msg.id, "abc-123");
145        assert_eq!(msg.chatroom_id, Some(12345));
146        assert_eq!(msg.content, "hello world");
147        assert_eq!(msg.r#type, "message");
148        assert_eq!(msg.sender.username, "hello_kiko");
149        assert_eq!(msg.sender.id, 9999);
150        assert_eq!(msg.sender.identity.color, "#FF0000");
151        assert!(msg.sender.identity.badges.is_empty());
152        assert!(msg.metadata.is_none());
153    }
154
155    #[test]
156    fn test_deserialize_chat_message_with_badges() {
157        let json = r##"{
158            "id": "msg-456",
159            "content": "gg",
160            "type": "message",
161            "sender": {
162                "id": 1234,
163                "username": "hello_kiko",
164                "identity": {
165                    "color": "#00FF00",
166                    "badges": [
167                        { "type": "subscriber", "text": "Subscriber", "count": 3 },
168                        { "type": "moderator", "text": "Moderator" }
169                    ]
170                }
171            }
172        }"##;
173
174        let msg: LiveChatMessage = serde_json::from_str(json).unwrap();
175        assert_eq!(msg.sender.identity.badges.len(), 2);
176        assert_eq!(msg.sender.identity.badges[0].r#type, "subscriber");
177        assert_eq!(msg.sender.identity.badges[0].count, Some(3));
178        assert_eq!(msg.sender.identity.badges[1].r#type, "moderator");
179        assert_eq!(msg.sender.identity.badges[1].count, None);
180        // Optional fields should be None when missing
181        assert!(msg.chatroom_id.is_none());
182        assert!(msg.created_at.is_none());
183        assert!(msg.sender.slug.is_none());
184    }
185
186    #[test]
187    fn test_deserialize_reply_message() {
188        let json = r##"{
189            "id": "reply-789",
190            "content": "I agree!",
191            "type": "reply",
192            "sender": {
193                "id": 5555,
194                "username": "test_user",
195                "identity": {
196                    "color": "#0000FF",
197                    "badges": []
198                }
199            },
200            "metadata": {
201                "original_sender": { "username": "hello_kiko" },
202                "original_message": { "content": "what do you think?" }
203            }
204        }"##;
205
206        let msg: LiveChatMessage = serde_json::from_str(json).unwrap();
207        assert_eq!(msg.r#type, "reply");
208        let meta = msg.metadata.unwrap();
209        assert_eq!(meta.original_sender.unwrap().username, "hello_kiko");
210        assert_eq!(meta.original_message.unwrap().content, "what do you think?");
211    }
212
213    #[test]
214    fn test_deserialize_pusher_message() {
215        // Pusher sends the event name with backslash-separated namespaces
216        // and double-encodes the data as a JSON string inside JSON.
217        let inner_data = serde_json::json!({
218            "id": "msg-1",
219            "content": "test",
220            "type": "message",
221            "sender": {
222                "id": 1,
223                "username": "hello_kiko",
224                "identity": { "color": "#FFF", "badges": [] }
225            }
226        });
227
228        let outer = serde_json::json!({
229            "event": "App\\Events\\ChatMessageEvent",
230            "data": inner_data.to_string(),
231            "channel": "chatrooms.12345.v2"
232        });
233
234        let pusher: PusherMessage = serde_json::from_value(outer).unwrap();
235        assert!(pusher.event.contains("ChatMessageEvent"));
236        assert_eq!(pusher.channel, Some("chatrooms.12345.v2".into()));
237
238        // Verify the double-encoded data can be parsed
239        let msg: LiveChatMessage = serde_json::from_str(&pusher.data).unwrap();
240        assert_eq!(msg.sender.username, "hello_kiko");
241        assert_eq!(msg.content, "test");
242    }
243}