Skip to main content

rustant_core/channels/
normalize.rs

1//! Message normalizer — converts platform-specific message formats into
2//! the unified `ChannelMessage` type.
3
4use super::{ChannelMessage, ChannelType, ChannelUser, MessageContent};
5
6/// Normalizes platform-specific messages into unified ChannelMessages.
7pub struct MessageNormalizer;
8
9impl MessageNormalizer {
10    /// Normalize a raw text string from Telegram into a ChannelMessage.
11    pub fn normalize_telegram(
12        chat_id: i64,
13        user_id: i64,
14        user_name: &str,
15        text: &str,
16    ) -> ChannelMessage {
17        let sender =
18            ChannelUser::new(user_id.to_string(), ChannelType::Telegram).with_name(user_name);
19        let content = if text.starts_with('/') {
20            let parts: Vec<&str> = text.splitn(2, ' ').collect();
21            let cmd = parts[0].to_string();
22            let args = if parts.len() > 1 {
23                parts[1].split_whitespace().map(String::from).collect()
24            } else {
25                Vec::new()
26            };
27            MessageContent::command(cmd, args)
28        } else {
29            MessageContent::text(text)
30        };
31        ChannelMessage {
32            id: super::MessageId::random(),
33            channel_type: ChannelType::Telegram,
34            channel_id: chat_id.to_string(),
35            sender,
36            content,
37            timestamp: chrono::Utc::now(),
38            reply_to: None,
39            thread_id: None,
40            metadata: std::collections::HashMap::new(),
41        }
42    }
43
44    /// Normalize a raw Discord message.
45    pub fn normalize_discord(
46        channel_id: &str,
47        author_id: &str,
48        author_name: &str,
49        content: &str,
50    ) -> ChannelMessage {
51        let sender = ChannelUser::new(author_id, ChannelType::Discord).with_name(author_name);
52        ChannelMessage::text(ChannelType::Discord, channel_id, sender, content)
53    }
54
55    /// Normalize a raw Slack message.
56    pub fn normalize_slack(
57        channel: &str,
58        user: &str,
59        text: &str,
60        thread_ts: Option<&str>,
61    ) -> ChannelMessage {
62        let sender = ChannelUser::new(user, ChannelType::Slack);
63        let mut msg = ChannelMessage::text(ChannelType::Slack, channel, sender, text);
64        if let Some(ts) = thread_ts {
65            msg = msg.with_metadata("thread_ts", ts);
66        }
67        msg
68    }
69
70    /// Normalize a raw email into a ChannelMessage.
71    pub fn normalize_email(from: &str, subject: &str, body: &str) -> ChannelMessage {
72        let sender = ChannelUser::new(from, ChannelType::Email);
73        ChannelMessage::text(ChannelType::Email, from, sender, body)
74            .with_metadata("subject", subject)
75    }
76
77    /// Normalize an iMessage incoming message.
78    pub fn normalize_imessage(sender_id: &str, text: &str) -> ChannelMessage {
79        let sender = ChannelUser::new(sender_id, ChannelType::IMessage);
80        ChannelMessage::text(ChannelType::IMessage, sender_id, sender, text)
81    }
82
83    /// Normalize a Microsoft Teams message.
84    pub fn normalize_teams(
85        channel_id: &str,
86        from_id: &str,
87        from_name: &str,
88        content: &str,
89    ) -> ChannelMessage {
90        let sender = ChannelUser::new(from_id, ChannelType::Teams).with_name(from_name);
91        ChannelMessage::text(ChannelType::Teams, channel_id, sender, content)
92    }
93
94    /// Normalize an SMS message.
95    pub fn normalize_sms(from_number: &str, body: &str) -> ChannelMessage {
96        let sender = ChannelUser::new(from_number, ChannelType::Sms);
97        ChannelMessage::text(ChannelType::Sms, from_number, sender, body)
98    }
99
100    /// Normalize an IRC PRIVMSG.
101    pub fn normalize_irc(nick: &str, channel: &str, text: &str) -> ChannelMessage {
102        let sender = ChannelUser::new(nick, ChannelType::Irc);
103        ChannelMessage::text(ChannelType::Irc, channel, sender, text)
104    }
105
106    /// Normalize a webhook payload.
107    pub fn normalize_webhook(source: &str, body: &str) -> ChannelMessage {
108        let sender = ChannelUser::new(source, ChannelType::Webhook);
109        ChannelMessage::text(ChannelType::Webhook, source, sender, body)
110    }
111
112    /// Normalize a media message (audio, video, etc.) from any channel.
113    pub fn normalize_media(
114        channel_type: ChannelType,
115        channel_id: &str,
116        sender_id: &str,
117        url: &str,
118        mime_type: &str,
119        caption: Option<&str>,
120    ) -> ChannelMessage {
121        let sender = ChannelUser::new(sender_id, channel_type);
122        ChannelMessage {
123            id: super::MessageId::random(),
124            channel_type,
125            channel_id: channel_id.to_string(),
126            sender,
127            content: MessageContent::Media {
128                url: url.to_string(),
129                mime_type: mime_type.to_string(),
130                caption: caption.map(|c| c.to_string()),
131            },
132            timestamp: chrono::Utc::now(),
133            reply_to: None,
134            thread_id: None,
135            metadata: std::collections::HashMap::new(),
136        }
137    }
138
139    /// Normalize a location message from any channel.
140    pub fn normalize_location(
141        channel_type: ChannelType,
142        channel_id: &str,
143        sender_id: &str,
144        latitude: f64,
145        longitude: f64,
146        label: Option<&str>,
147    ) -> ChannelMessage {
148        let sender = ChannelUser::new(sender_id, channel_type);
149        ChannelMessage {
150            id: super::MessageId::random(),
151            channel_type,
152            channel_id: channel_id.to_string(),
153            sender,
154            content: MessageContent::Location {
155                latitude,
156                longitude,
157                label: label.map(|l| l.to_string()),
158            },
159            timestamp: chrono::Utc::now(),
160            reply_to: None,
161            thread_id: None,
162            metadata: std::collections::HashMap::new(),
163        }
164    }
165
166    /// Normalize a contact message from any channel.
167    pub fn normalize_contact(
168        channel_type: ChannelType,
169        channel_id: &str,
170        sender_id: &str,
171        name: &str,
172        phone: Option<&str>,
173        email: Option<&str>,
174    ) -> ChannelMessage {
175        let sender = ChannelUser::new(sender_id, channel_type);
176        ChannelMessage {
177            id: super::MessageId::random(),
178            channel_type,
179            channel_id: channel_id.to_string(),
180            sender,
181            content: MessageContent::Contact {
182                name: name.to_string(),
183                phone: phone.map(|p| p.to_string()),
184                email: email.map(|e| e.to_string()),
185            },
186            timestamp: chrono::Utc::now(),
187            reply_to: None,
188            thread_id: None,
189            metadata: std::collections::HashMap::new(),
190        }
191    }
192
193    /// Normalize a reaction message from any channel.
194    pub fn normalize_reaction(
195        channel_type: ChannelType,
196        channel_id: &str,
197        sender_id: &str,
198        emoji: &str,
199        target_message_id: super::MessageId,
200    ) -> ChannelMessage {
201        let sender = ChannelUser::new(sender_id, channel_type);
202        ChannelMessage {
203            id: super::MessageId::random(),
204            channel_type,
205            channel_id: channel_id.to_string(),
206            sender,
207            content: MessageContent::Reaction {
208                emoji: emoji.to_string(),
209                target_message_id,
210            },
211            timestamp: chrono::Utc::now(),
212            reply_to: None,
213            thread_id: None,
214            metadata: std::collections::HashMap::new(),
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_normalize_telegram_text() {
225        let msg = MessageNormalizer::normalize_telegram(12345, 42, "Alice", "hello world");
226        assert_eq!(msg.channel_type, ChannelType::Telegram);
227        assert_eq!(msg.channel_id, "12345");
228        assert_eq!(msg.content.as_text(), Some("hello world"));
229        assert_eq!(msg.sender.display_name.as_deref(), Some("Alice"));
230    }
231
232    #[test]
233    fn test_normalize_telegram_command() {
234        let msg = MessageNormalizer::normalize_telegram(12345, 42, "Alice", "/help topic");
235        match &msg.content {
236            MessageContent::Command { command, args } => {
237                assert_eq!(command, "/help");
238                assert_eq!(args, &["topic"]);
239            }
240            _ => panic!("Expected Command"),
241        }
242    }
243
244    #[test]
245    fn test_normalize_discord() {
246        let msg = MessageNormalizer::normalize_discord("ch1", "u1", "Bob", "hey discord");
247        assert_eq!(msg.channel_type, ChannelType::Discord);
248        assert_eq!(msg.content.as_text(), Some("hey discord"));
249    }
250
251    #[test]
252    fn test_normalize_slack_with_thread() {
253        let msg =
254            MessageNormalizer::normalize_slack("general", "U123", "hi slack", Some("1234.5678"));
255        assert_eq!(msg.channel_type, ChannelType::Slack);
256        assert_eq!(msg.content.as_text(), Some("hi slack"));
257        assert_eq!(
258            msg.metadata.get("thread_ts").map(|s| s.as_str()),
259            Some("1234.5678")
260        );
261    }
262
263    #[test]
264    fn test_normalize_email() {
265        let msg = MessageNormalizer::normalize_email("alice@ex.com", "Subject Line", "body text");
266        assert_eq!(msg.channel_type, ChannelType::Email);
267        assert_eq!(msg.content.as_text(), Some("body text"));
268        assert_eq!(
269            msg.metadata.get("subject").map(|s| s.as_str()),
270            Some("Subject Line")
271        );
272    }
273
274    #[test]
275    fn test_normalize_imessage() {
276        let msg = MessageNormalizer::normalize_imessage("+1234567890", "hi from imsg");
277        assert_eq!(msg.channel_type, ChannelType::IMessage);
278        assert_eq!(msg.content.as_text(), Some("hi from imsg"));
279        assert_eq!(msg.sender.id, "+1234567890");
280    }
281
282    #[test]
283    fn test_normalize_teams() {
284        let msg = MessageNormalizer::normalize_teams("ch1", "u1", "Alice", "teams msg");
285        assert_eq!(msg.channel_type, ChannelType::Teams);
286        assert_eq!(msg.content.as_text(), Some("teams msg"));
287        assert_eq!(msg.sender.display_name.as_deref(), Some("Alice"));
288    }
289
290    #[test]
291    fn test_normalize_sms() {
292        let msg = MessageNormalizer::normalize_sms("+9876543210", "sms body");
293        assert_eq!(msg.channel_type, ChannelType::Sms);
294        assert_eq!(msg.content.as_text(), Some("sms body"));
295    }
296
297    #[test]
298    fn test_normalize_irc() {
299        let msg = MessageNormalizer::normalize_irc("nick42", "#channel", "irc msg");
300        assert_eq!(msg.channel_type, ChannelType::Irc);
301        assert_eq!(msg.channel_id, "#channel");
302        assert_eq!(msg.content.as_text(), Some("irc msg"));
303    }
304
305    #[test]
306    fn test_normalize_webhook() {
307        let msg = MessageNormalizer::normalize_webhook("ext-system", "webhook data");
308        assert_eq!(msg.channel_type, ChannelType::Webhook);
309        assert_eq!(msg.content.as_text(), Some("webhook data"));
310    }
311
312    #[test]
313    fn test_normalize_media_content() {
314        let msg = MessageNormalizer::normalize_media(
315            ChannelType::Telegram,
316            "chat1",
317            "user1",
318            "https://example.com/video.mp4",
319            "video/mp4",
320            Some("My video"),
321        );
322        assert_eq!(msg.channel_type, ChannelType::Telegram);
323        match &msg.content {
324            MessageContent::Media {
325                url,
326                mime_type,
327                caption,
328            } => {
329                assert_eq!(url, "https://example.com/video.mp4");
330                assert_eq!(mime_type, "video/mp4");
331                assert_eq!(caption.as_deref(), Some("My video"));
332            }
333            _ => panic!("Expected Media"),
334        }
335    }
336
337    #[test]
338    fn test_normalize_location_content() {
339        let msg = MessageNormalizer::normalize_location(
340            ChannelType::WhatsApp,
341            "chat1",
342            "user1",
343            37.7749,
344            -122.4194,
345            Some("San Francisco"),
346        );
347        assert_eq!(msg.channel_type, ChannelType::WhatsApp);
348        match &msg.content {
349            MessageContent::Location {
350                latitude,
351                longitude,
352                label,
353            } => {
354                assert!((latitude - 37.7749).abs() < f64::EPSILON);
355                assert!((longitude - (-122.4194)).abs() < f64::EPSILON);
356                assert_eq!(label.as_deref(), Some("San Francisco"));
357            }
358            _ => panic!("Expected Location"),
359        }
360    }
361
362    #[test]
363    fn test_normalize_contact_content() {
364        let msg = MessageNormalizer::normalize_contact(
365            ChannelType::Telegram,
366            "chat1",
367            "user1",
368            "Jane Doe",
369            Some("+1234567890"),
370            Some("jane@example.com"),
371        );
372        assert_eq!(msg.channel_type, ChannelType::Telegram);
373        match &msg.content {
374            MessageContent::Contact { name, phone, email } => {
375                assert_eq!(name, "Jane Doe");
376                assert_eq!(phone.as_deref(), Some("+1234567890"));
377                assert_eq!(email.as_deref(), Some("jane@example.com"));
378            }
379            _ => panic!("Expected Contact"),
380        }
381    }
382
383    #[test]
384    fn test_normalize_reaction_content() {
385        let target_id = crate::channels::MessageId::random();
386        let msg = MessageNormalizer::normalize_reaction(
387            ChannelType::Slack,
388            "general",
389            "user1",
390            "👍",
391            target_id.clone(),
392        );
393        assert_eq!(msg.channel_type, ChannelType::Slack);
394        match &msg.content {
395            MessageContent::Reaction {
396                emoji,
397                target_message_id,
398            } => {
399                assert_eq!(emoji, "👍");
400                assert_eq!(target_message_id, &target_id);
401            }
402            _ => panic!("Expected Reaction"),
403        }
404    }
405}