kick_rust/
message_parser.rs

1//! Message parser for processing Kick.com WebSocket events
2
3use crate::types::*;
4use tracing::debug;
5use regex::Regex;
6use std::collections::HashMap;
7
8/// Parser for processing WebSocket messages from Kick.com
9pub struct MessageParser;
10
11impl MessageParser {
12    /// Helper function to extract data from either a JSON object or a JSON string
13    fn extract_data<T: serde::de::DeserializeOwned>(data: serde_json::Value) -> Result<T> {
14        match data {
15            serde_json::Value::String(s) => {
16                // Data is a string containing JSON - parse it
17                debug!("Extracting data from JSON string: {}", s);
18                serde_json::from_str(&s).map_err(|e| KickError::InvalidMessage(format!("Failed to parse JSON string: {}", e)))
19            }
20            serde_json::Value::Object(_) => {
21                // Data is already a JSON object - parse it directly
22                debug!("Extracting data from JSON object");
23                serde_json::from_value(data).map_err(|e| KickError::InvalidMessage(format!("Failed to parse JSON object: {}", e)))
24            }
25            _ => {
26                debug!("Data is neither a string nor an object: {:?}", data);
27                Err(KickError::InvalidMessage("Data is neither a string nor an object".to_string()))
28            }
29        }
30    }
31
32    /// Parse a raw WebSocket message and return the processed event
33    pub fn parse_message(raw_message: &str) -> Result<Option<ParsedMessage>> {
34        // Validate that the message is not empty
35        if raw_message.trim().is_empty() {
36            return Err(KickError::InvalidMessage("Empty message".to_string()));
37        }
38
39        let message: WebSocketMessage = serde_json::from_str(raw_message)?;
40
41        // Validate that the event exists
42        if message.event.is_empty() {
43            return Err(KickError::InvalidMessage("Empty event".to_string()));
44        }
45
46        // Ignore Pusher system events - return None instead of error
47        if message.event.starts_with("pusher:") || message.event.starts_with("pusher_internal:") {
48            return Ok(None);
49        }
50
51        // Map events to standard types and parse data
52        match message.event.as_str() {
53            "App\\Events\\ChatMessageEvent" => {
54                let data: RawChatMessageData = Self::extract_data(message.data)?;
55                Ok(Some(ParsedMessage {
56                    r#type: KickEventType::ChatMessage,
57                    data: KickEventData::ChatMessage(Self::parse_chat_message(data)),
58                }))
59            }
60            "App\\Events\\MessageDeletedEvent" => {
61                let data: RawMessageDeletedData = Self::extract_data(message.data)?;
62                Ok(Some(ParsedMessage {
63                    r#type: KickEventType::MessageDeleted,
64                    data: KickEventData::MessageDeleted(Self::parse_message_deleted(data)),
65                }))
66            }
67            "App\\Events\\UserBannedEvent" => {
68                let data: RawUserBannedData = Self::extract_data(message.data)?;
69                Ok(Some(ParsedMessage {
70                    r#type: KickEventType::UserBanned,
71                    data: KickEventData::UserBanned(Self::parse_user_banned(data)),
72                }))
73            }
74            "App\\Events\\UserUnbannedEvent" => {
75                let data: RawUserUnbannedData = Self::extract_data(message.data)?;
76                Ok(Some(ParsedMessage {
77                    r#type: KickEventType::UserUnbanned,
78                    data: KickEventData::UserUnbanned(Self::parse_user_unbanned(data)),
79                }))
80            }
81            "App\\Events\\SubscriptionEvent" => {
82                let data: RawSubscriptionData = Self::extract_data(message.data)?;
83                Ok(Some(ParsedMessage {
84                    r#type: KickEventType::Subscription,
85                    data: KickEventData::Subscription(Self::parse_subscription(data)),
86                }))
87            }
88            "App\\Events\\GiftedSubscriptionsEvent" => {
89                let data: RawGiftedSubscriptionsData = Self::extract_data(message.data)?;
90                Ok(Some(ParsedMessage {
91                    r#type: KickEventType::GiftedSubscriptions,
92                    data: KickEventData::GiftedSubscriptions(Self::parse_gifted_subscriptions(data)),
93                }))
94            }
95            "App\\Events\\PinnedMessageCreatedEvent" => {
96                let data: RawPinnedMessageCreatedData = Self::extract_data(message.data)?;
97                Ok(Some(ParsedMessage {
98                    r#type: KickEventType::PinnedMessageCreated,
99                    data: KickEventData::PinnedMessageCreated(Self::parse_pinned_message_created(data)),
100                }))
101            }
102            "App\\Events\\StreamHostEvent" => {
103                let data: RawStreamHostData = Self::extract_data(message.data)?;
104                Ok(Some(ParsedMessage {
105                    r#type: KickEventType::StreamHost,
106                    data: KickEventData::StreamHost(Self::parse_stream_host(data)),
107                }))
108            }
109            "App\\Events\\PollUpdateEvent" => {
110                let data: RawPollUpdateData = Self::extract_data(message.data)?;
111                Ok(Some(ParsedMessage {
112                    r#type: KickEventType::PollUpdate,
113                    data: KickEventData::PollUpdate(Self::parse_poll_update(data)),
114                }))
115            }
116            "App\\Events\\PollDeleteEvent" => {
117                let data: RawPollDeleteData = Self::extract_data(message.data)?;
118                Ok(Some(ParsedMessage {
119                    r#type: KickEventType::PollDelete,
120                    data: KickEventData::PollDelete(Self::parse_poll_delete(data)),
121                }))
122            }
123            _ => Err(KickError::UnknownEventType(message.event)),
124        }
125    }
126
127    /// Parse a chat message event
128    pub fn parse_chat_message(data: RawChatMessageData) -> ChatMessageEvent {
129        ChatMessageEvent {
130            id: data.id,
131            content: Self::clean_emotes(&data.content),
132            message_type: data.message_type,
133            created_at: data.created_at,
134            sender: data.sender,
135            chatroom: KickChatroom {
136                id: data.chatroom_id,
137                channel_id: data.chatroom_id,
138                name: String::new(), // Empty name since it's not provided
139            },
140        }
141    }
142
143    /// Parse a message deleted event
144    pub fn parse_message_deleted(data: RawMessageDeletedData) -> MessageDeletedEvent {
145        MessageDeletedEvent {
146            message_id: data.message_id,
147            chatroom_id: data.chatroom_id,
148            event_type: "message_deleted".to_string(),
149        }
150    }
151
152    /// Parse a user banned event
153    pub fn parse_user_banned(data: RawUserBannedData) -> UserBannedEvent {
154        let username = data.username
155            .or(data.banned_username)
156            .unwrap_or_else(|| "unknown".to_string());
157
158        UserBannedEvent {
159            username,
160            event_type: "user_banned".to_string(),
161        }
162    }
163
164    /// Parse a user unbanned event
165    pub fn parse_user_unbanned(data: RawUserUnbannedData) -> UserUnbannedEvent {
166        let username = data.username.unwrap_or_else(|| "unknown".to_string());
167
168        UserUnbannedEvent {
169            username,
170            event_type: "user_unbanned".to_string(),
171        }
172    }
173
174    /// Parse a subscription event
175    pub fn parse_subscription(data: RawSubscriptionData) -> SubscriptionEvent {
176        SubscriptionEvent {
177            username: data.username,
178            months: data.months,
179            event_type: "subscription".to_string(),
180        }
181    }
182
183    /// Parse a gifted subscriptions event
184    pub fn parse_gifted_subscriptions(data: RawGiftedSubscriptionsData) -> GiftedSubscriptionsEvent {
185        let gifter = data.gifted_by
186            .or_else(|| {
187                if let Some(gifter_value) = &data.gifter {
188                    if let Some(gifter_str) = gifter_value.as_str() {
189                        Some(gifter_str.to_string())
190                    } else if let Some(gifter_obj) = gifter_value.as_object() {
191                        gifter_obj.get("username")
192                            .and_then(|v| v.as_str())
193                            .map(|s| s.to_string())
194                    } else {
195                        None
196                    }
197                } else {
198                    None
199                }
200            })
201            .unwrap_or_else(|| "unknown".to_string());
202
203        let recipients = data.recipients
204            .into_iter()
205            .map(|r| {
206                if let Some(r_str) = r.as_str() {
207                    r_str.to_string()
208                } else if let Some(r_obj) = r.as_object() {
209                    r_obj.get("username")
210                        .and_then(|v| v.as_str())
211                        .unwrap_or("unknown")
212                        .to_string()
213                } else {
214                    "unknown".to_string()
215                }
216            })
217            .collect();
218
219        GiftedSubscriptionsEvent {
220            gifted_by: gifter,
221            recipients,
222            event_type: "gifted_subscriptions".to_string(),
223        }
224    }
225
226    /// Parse a pinned message created event
227    pub fn parse_pinned_message_created(data: RawPinnedMessageCreatedData) -> PinnedMessageCreatedEvent {
228        PinnedMessageCreatedEvent {
229            message: Self::parse_chat_message(data.message),
230            event_type: "pinned_message_created".to_string(),
231        }
232    }
233
234    /// Parse a stream host event
235    pub fn parse_stream_host(data: RawStreamHostData) -> StreamHostEvent {
236        let hoster = if let Some(hoster_str) = data.hoster.as_str() {
237            hoster_str.to_string()
238        } else if let Some(hoster_obj) = data.hoster.as_object() {
239            hoster_obj.get("username")
240                .and_then(|v| v.as_str())
241                .unwrap_or("unknown")
242                .to_string()
243        } else {
244            "unknown".to_string()
245        };
246
247        let hosted_channel = if let Some(hosted_str) = data.hosted_channel.as_str() {
248            hosted_str.to_string()
249        } else if let Some(hosted_obj) = data.hosted_channel.as_object() {
250            hosted_obj.get("username")
251                .and_then(|v| v.as_str())
252                .unwrap_or("unknown")
253                .to_string()
254        } else {
255            "unknown".to_string()
256        };
257
258        StreamHostEvent {
259            hoster,
260            hosted_channel,
261            event_type: "stream_host".to_string(),
262        }
263    }
264
265    /// Parse a poll update event
266    pub fn parse_poll_update(data: RawPollUpdateData) -> PollUpdateEvent {
267        let options = data.options
268            .into_iter()
269            .map(|opt| PollOption {
270                id: opt.id,
271                text: opt.text,
272                votes: opt.votes.unwrap_or(0),
273            })
274            .collect();
275
276        PollUpdateEvent {
277            poll_id: data.id,
278            question: data.question,
279            options,
280            event_type: "poll_update".to_string(),
281        }
282    }
283
284    /// Parse a poll delete event
285    pub fn parse_poll_delete(data: RawPollDeleteData) -> PollDeleteEvent {
286        PollDeleteEvent {
287            poll_id: data.id,
288            event_type: "poll_delete".to_string(),
289        }
290    }
291
292    /// Clean emote codes from message content
293    pub fn clean_emotes(content: &str) -> String {
294        if content.is_empty() {
295            return String::new();
296        }
297
298        // Replace emote codes like [emote:123:emoteName] with the emote name
299        let emote_regex = Regex::new(r"\[emote:(\d+):(\w+)\]").unwrap();
300        emote_regex.replace_all(content, "$2").to_string()
301    }
302
303    /// Check if a message is valid
304    pub fn is_valid_message(message: &str) -> bool {
305        if message.trim().is_empty() {
306            return false;
307        }
308
309        match serde_json::from_str::<WebSocketMessage>(message) {
310            Ok(parsed) => {
311                !parsed.event.is_empty() && parsed.data != serde_json::Value::Null
312            }
313            Err(_) => false,
314        }
315    }
316
317    /// Check if a message is a Kick event (not Pusher system event)
318    pub fn is_kick_event(message: &str) -> bool {
319        if let Ok(parsed) = serde_json::from_str::<WebSocketMessage>(message) {
320            !parsed.event.starts_with("pusher:") &&
321            !parsed.event.starts_with("pusher_internal:") &&
322            !parsed.event.is_empty()
323        } else {
324            false
325        }
326    }
327
328    /// Extract the event type from a raw message
329    pub fn extract_event_type(raw_message: &str) -> Result<String> {
330        if raw_message.trim().is_empty() {
331            return Err(KickError::InvalidMessage("Empty message".to_string()));
332        }
333
334        let message: WebSocketMessage = serde_json::from_str(raw_message)?;
335
336        if message.event.is_empty() {
337            return Err(KickError::InvalidMessage("Empty event".to_string()));
338        }
339
340        // Ignore Pusher system events
341        if message.event.starts_with("pusher:") || message.event.starts_with("pusher_internal:") {
342            return Err(KickError::InvalidMessage(format!("Pusher system event: {}", message.event)));
343        }
344
345        Ok(message.event)
346    }
347
348    /// Map Kick event names to standard event types
349    pub fn map_kick_event_to_standard(event_name: &str) -> Option<KickEventType> {
350        let event_map: HashMap<&str, KickEventType> = HashMap::from([
351            ("App\\Events\\ChatMessageEvent", KickEventType::ChatMessage),
352            ("App\\Events\\MessageDeletedEvent", KickEventType::MessageDeleted),
353            ("App\\Events\\UserBannedEvent", KickEventType::UserBanned),
354            ("App\\Events\\UserUnbannedEvent", KickEventType::UserUnbanned),
355            ("App\\Events\\SubscriptionEvent", KickEventType::Subscription),
356            ("App\\Events\\GiftedSubscriptionsEvent", KickEventType::GiftedSubscriptions),
357            ("App\\Events\\PinnedMessageCreatedEvent", KickEventType::PinnedMessageCreated),
358            ("App\\Events\\StreamHostEvent", KickEventType::StreamHost),
359            ("App\\Events\\PollUpdateEvent", KickEventType::PollUpdate),
360            ("App\\Events\\PollDeleteEvent", KickEventType::PollDelete),
361        ]);
362
363        event_map.get(event_name).cloned()
364    }
365}
366
367/// Parse a custom message structure from raw JSON data
368pub fn parse_custom_message<T: serde::de::DeserializeOwned>(raw_message: &str) -> crate::types::Result<T> {
369    serde_json::from_str(raw_message)
370        .map_err(|e| KickError::InvalidMessage(format!("Failed to parse custom message: {}", e)))
371}
372
373/// Extract a field from a RawMessage using dot notation
374pub fn extract_field(raw_message: &RawMessage, field_path: &str) -> Option<serde_json::Value> {
375    // Parse the raw JSON
376    let parsed: serde_json::Value = serde_json::from_str(&raw_message.raw_json).ok()?;
377
378    // Split the field path by dots and navigate the JSON
379    let mut current = &parsed;
380    for part in field_path.split('.') {
381        match current {
382            serde_json::Value::Object(map) => {
383                current = map.get(part)?;
384            }
385            serde_json::Value::Array(arr) => {
386                if let Ok(index) = part.parse::<usize>() {
387                    current = arr.get(index)?;
388                } else {
389                    return None;
390                }
391            }
392            _ => return None,
393        }
394    }
395
396    Some(current.clone())
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_clean_emotes() {
405        let content = "Hello [emote:123:wave] world [emote:456:heart]!";
406        let cleaned = MessageParser::clean_emotes(content);
407        assert_eq!(cleaned, "Hello wave world heart!");
408    }
409
410    #[test]
411    fn test_empty_content() {
412        let cleaned = MessageParser::clean_emotes("");
413        assert_eq!(cleaned, "");
414    }
415
416    #[test]
417    fn test_is_valid_message() {
418        let valid_json = r#"{"event":"test","data":{"key":"value"}}"#;
419        let invalid_json = r#"{"event":"test"}"#;
420        let empty = "";
421
422        assert!(MessageParser::is_valid_message(valid_json));
423        assert!(!MessageParser::is_valid_message(invalid_json));
424        assert!(!MessageParser::is_valid_message(empty));
425    }
426
427    #[test]
428    fn test_extract_event_type() {
429        let message = r#"{"event":"App\\Events\\ChatMessageEvent","data":{}}"#;
430        let event_type = MessageParser::extract_event_type(message).unwrap();
431        assert_eq!(event_type, "App\\Events\\ChatMessageEvent");
432
433        // Test that Pusher events return None
434        let pusher_message = r#"{"event":"pusher:connection_established","data":"{}"}"#;
435        let result = MessageParser::parse_message(pusher_message);
436        assert!(result.is_ok());
437        assert!(result.unwrap().is_none());
438    }
439
440    #[test]
441    fn test_map_kick_event_to_standard() {
442        assert_eq!(
443            MessageParser::map_kick_event_to_standard("App\\Events\\ChatMessageEvent"),
444            Some(KickEventType::ChatMessage)
445        );
446        assert_eq!(
447            MessageParser::map_kick_event_to_standard("UnknownEvent"),
448            None
449        );
450    }
451}