1use std::collections::HashMap;
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct InboundMessage {
18 pub channel: String,
20
21 pub sender_id: String,
23
24 pub chat_id: String,
26
27 pub content: String,
29
30 #[serde(default = "Utc::now")]
32 pub timestamp: DateTime<Utc>,
33
34 #[serde(default)]
36 pub media: Vec<String>,
37
38 #[serde(default)]
40 pub metadata: HashMap<String, serde_json::Value>,
41}
42
43impl InboundMessage {
44 pub fn session_key(&self) -> String {
46 format!("{}:{}", self.channel, self.chat_id)
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct OutboundMessage {
56 pub channel: String,
58
59 pub chat_id: String,
61
62 pub content: String,
64
65 #[serde(default)]
67 pub reply_to: Option<String>,
68
69 #[serde(default)]
71 pub media: Vec<String>,
72
73 #[serde(default)]
75 pub metadata: HashMap<String, serde_json::Value>,
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn inbound_session_key() {
84 let msg = InboundMessage {
85 channel: "telegram".into(),
86 sender_id: "user123".into(),
87 chat_id: "chat456".into(),
88 content: "hello".into(),
89 timestamp: Utc::now(),
90 media: vec![],
91 metadata: HashMap::new(),
92 };
93 assert_eq!(msg.session_key(), "telegram:chat456");
94 }
95
96 #[test]
97 fn inbound_serde_roundtrip() {
98 let msg = InboundMessage {
99 channel: "slack".into(),
100 sender_id: "U12345".into(),
101 chat_id: "C67890".into(),
102 content: "test message".into(),
103 timestamp: Utc::now(),
104 media: vec!["https://example.com/image.png".into()],
105 metadata: {
106 let mut m = HashMap::new();
107 m.insert("thread_ts".into(), serde_json::json!("123.456"));
108 m
109 },
110 };
111 let json = serde_json::to_string(&msg).unwrap();
112 let restored: InboundMessage = serde_json::from_str(&json).unwrap();
113 assert_eq!(restored.channel, "slack");
114 assert_eq!(restored.sender_id, "U12345");
115 assert_eq!(restored.chat_id, "C67890");
116 assert_eq!(restored.content, "test message");
117 assert_eq!(restored.media.len(), 1);
118 assert!(restored.metadata.contains_key("thread_ts"));
119 }
120
121 #[test]
122 fn inbound_defaults_on_missing_fields() {
123 let json = r#"{
124 "channel": "discord",
125 "sender_id": "u1",
126 "chat_id": "c1",
127 "content": "hi"
128 }"#;
129 let msg: InboundMessage = serde_json::from_str(json).unwrap();
130 assert!(msg.media.is_empty());
131 assert!(msg.metadata.is_empty());
132 }
133
134 #[test]
135 fn outbound_serde_roundtrip() {
136 let msg = OutboundMessage {
137 channel: "telegram".into(),
138 chat_id: "chat456".into(),
139 content: "reply".into(),
140 reply_to: Some("msg789".into()),
141 media: vec![],
142 metadata: HashMap::new(),
143 };
144 let json = serde_json::to_string(&msg).unwrap();
145 let restored: OutboundMessage = serde_json::from_str(&json).unwrap();
146 assert_eq!(restored.channel, "telegram");
147 assert_eq!(restored.reply_to.as_deref(), Some("msg789"));
148 }
149
150 #[test]
151 fn outbound_reply_to_optional() {
152 let json = r#"{
153 "channel": "slack",
154 "chat_id": "c1",
155 "content": "msg"
156 }"#;
157 let msg: OutboundMessage = serde_json::from_str(json).unwrap();
158 assert!(msg.reply_to.is_none());
159 assert!(msg.media.is_empty());
160 }
161}