agentmux_srv/messaging/whatsapp/
types.rs1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Default, Deserialize)]
19pub struct WebhookPayload {
20 #[serde(default)]
21 pub entry: Vec<WebhookEntry>,
22}
23
24#[derive(Debug, Default, Deserialize)]
25pub struct WebhookEntry {
26 #[serde(default)]
27 pub changes: Vec<WebhookChange>,
28}
29
30#[derive(Debug, Default, Deserialize)]
31pub struct WebhookChange {
32 #[serde(default)]
33 pub value: WebhookValue,
34}
35
36#[derive(Debug, Default, Deserialize)]
37pub struct WebhookValue {
38 #[serde(default)]
39 pub messages: Vec<WebhookMessage>,
40}
41
42#[derive(Debug, Clone, Deserialize)]
43pub struct WebhookMessage {
44 pub from: String,
46 pub id: String,
47 #[serde(default)]
48 pub text: Option<WebhookText>,
49}
50
51#[derive(Debug, Clone, Deserialize)]
52pub struct WebhookText {
53 pub body: String,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ExtractedMessage {
60 pub from: String,
61 pub id: String,
62 pub text: String,
63}
64
65impl WebhookPayload {
66 pub fn extract_messages(&self) -> Vec<ExtractedMessage> {
72 self.entry
73 .iter()
74 .flat_map(|e| e.changes.iter())
75 .flat_map(|c| c.value.messages.iter())
76 .filter_map(|m| {
77 let text = m.text.as_ref()?.body.clone();
78 Some(ExtractedMessage {
79 from: m.from.clone(),
80 id: m.id.clone(),
81 text,
82 })
83 })
84 .collect()
85 }
86}
87
88#[derive(Debug, Serialize)]
91pub struct SendBody {
92 pub messaging_product: &'static str,
93 pub to: String,
94 #[serde(rename = "type")]
95 pub msg_type: &'static str,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub text: Option<SendText>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub template: Option<SendTemplate>,
100}
101
102#[derive(Debug, Serialize)]
103pub struct SendText {
104 pub body: String,
105}
106
107#[derive(Debug, Serialize)]
108pub struct SendTemplate {
109 pub name: String,
110 pub language: SendTemplateLanguage,
111}
112
113#[derive(Debug, Serialize)]
114pub struct SendTemplateLanguage {
115 pub code: String,
116}
117
118impl SendBody {
119 pub fn text(to: &str, body: &str) -> Self {
120 SendBody {
121 messaging_product: "whatsapp",
122 to: to.to_string(),
123 msg_type: "text",
124 text: Some(SendText {
125 body: body.to_string(),
126 }),
127 template: None,
128 }
129 }
130
131 pub fn template(to: &str, name: &str, lang: &str) -> Self {
132 SendBody {
133 messaging_product: "whatsapp",
134 to: to.to_string(),
135 msg_type: "template",
136 text: None,
137 template: Some(SendTemplate {
138 name: name.to_string(),
139 language: SendTemplateLanguage {
140 code: lang.to_string(),
141 },
142 }),
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn extract_messages_flattens_nested_envelope_and_skips_non_text() {
153 let json = r#"{
154 "entry": [{
155 "changes": [{
156 "value": {
157 "messages": [
158 { "from": "15551234567", "id": "wamid.1", "type": "text", "text": { "body": "hi" } },
159 { "from": "15551234567", "id": "wamid.2", "type": "reaction" }
160 ]
161 }
162 }]
163 }]
164 }"#;
165 let payload: WebhookPayload = serde_json::from_str(json).unwrap();
166 let msgs = payload.extract_messages();
167 assert_eq!(msgs.len(), 1);
168 assert_eq!(msgs[0].from, "15551234567");
169 assert_eq!(msgs[0].id, "wamid.1");
170 assert_eq!(msgs[0].text, "hi");
171 }
172
173 #[test]
174 fn extract_messages_ignores_statuses_only_payload() {
175 let json = r#"{
179 "entry": [{
180 "changes": [{
181 "value": {
182 "statuses": [{ "id": "wamid.1", "status": "delivered" }]
183 }
184 }]
185 }]
186 }"#;
187 let payload: WebhookPayload = serde_json::from_str(json).unwrap();
188 assert!(payload.extract_messages().is_empty());
189 }
190
191 #[test]
192 fn extract_messages_empty_entry_list() {
193 let payload: WebhookPayload = serde_json::from_str(r#"{"entry": []}"#).unwrap();
194 assert!(payload.extract_messages().is_empty());
195 }
196
197 #[test]
198 fn send_body_text_serializes_expected_shape() {
199 let body = SendBody::text("15551234567", "hello");
200 let v = serde_json::to_value(&body).unwrap();
201 assert_eq!(v["messaging_product"], "whatsapp");
202 assert_eq!(v["to"], "15551234567");
203 assert_eq!(v["type"], "text");
204 assert_eq!(v["text"]["body"], "hello");
205 assert!(v.get("template").is_none());
206 }
207
208 #[test]
209 fn send_body_template_serializes_expected_shape() {
210 let body = SendBody::template("15551234567", "hello_world", "en_US");
211 let v = serde_json::to_value(&body).unwrap();
212 assert_eq!(v["type"], "template");
213 assert_eq!(v["template"]["name"], "hello_world");
214 assert_eq!(v["template"]["language"]["code"], "en_US");
215 assert!(v.get("text").is_none());
216 }
217}