1use super::{ChannelMessage, ChannelType, ChannelUser, MessageContent};
5
6pub struct MessageNormalizer;
8
9impl MessageNormalizer {
10 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 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 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 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 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 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 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 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 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 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 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 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 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}