1use crate::{ContactRef, RecordReference};
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, HashSet};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Conversation {
11 pub id: ConversationId,
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub contact: Option<ContactRef>,
14 pub items: Vec<ConversationItem>,
15 pub created: i64,
16 pub last_updated: i64,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
20pub struct ConversationId {
21 pub from: String, pub account: RecordReference,
23}
24
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ConversationItem {
30 pub id: String,
31 pub channel: String, pub timestamp: i64,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub voice: Option<VoiceItem>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub whatsapp: Option<WhatsAppItem>,
37 pub meta: HashSet<SessionVariable>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
43pub struct SessionVariable {
44 pub name: String,
45 pub value: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct VoiceItem {
52 pub id: String,
53 pub direction: String, pub origin: String,
55 pub from: String,
56 pub to: String,
57 pub call_sid: String,
58 pub call_id: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub x_cid: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub diversion: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub p_asserted_id: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub sbc_ip: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub sbc_trunk_name: Option<String>,
69 pub account_sid: String,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub application_sid: Option<String>,
72 pub timestamp: i64,
73 pub events: Vec<VoiceEvent>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct VoiceEvent {
78 #[serde(rename = "type")]
79 pub event_type: String, pub timestamp: i64,
81 pub attributes: HashMap<String, String>,
82}
83
84impl VoiceItem {
85 pub fn is_trying(&self) -> bool {
87 self.events.iter()
88 .filter(|e| e.event_type == "event")
89 .max_by_key(|e| e.timestamp)
90 .and_then(|e| e.attributes.get("sipStatus"))
91 .and_then(|s| s.parse::<i32>().ok())
92 .map(|status| status == 100)
93 .unwrap_or(false)
94 }
95
96 pub fn is_ringing(&self) -> bool {
97 self.events.iter()
98 .filter(|e| e.event_type == "event")
99 .max_by_key(|e| e.timestamp)
100 .and_then(|e| e.attributes.get("sipStatus"))
101 .and_then(|s| s.parse::<i32>().ok())
102 .map(|status| status > 100 && status < 200)
103 .unwrap_or(false)
104 }
105
106 pub fn is_connected(&self) -> bool {
107 self.events.iter()
108 .filter(|e| e.event_type == "event")
109 .max_by_key(|e| e.timestamp)
110 .and_then(|e| e.attributes.get("callStatus"))
111 .map(|status| status == "in-progress")
112 .unwrap_or(false)
113 }
114
115 pub fn is_completed(&self) -> bool {
116 self.events.iter()
117 .filter(|e| e.event_type == "event")
118 .max_by_key(|e| e.timestamp)
119 .and_then(|e| e.attributes.get("callStatus"))
120 .map(|status| status == "completed")
121 .unwrap_or(false)
122 }
123
124 pub fn is_recording(&self) -> bool {
125 self.events.iter()
126 .filter(|e| e.event_type == "recording-event")
127 .filter(|e| e.attributes.get("type").map(|t| t == "dial").unwrap_or(false))
128 .max_by_key(|e| e.timestamp)
129 .and_then(|e| e.attributes.get("status"))
130 .map(|status| status == "start")
131 .unwrap_or(false)
132 }
133
134 pub fn parked_calls(&self) -> Vec<&VoiceEvent> {
135 let mut parked_events = Vec::new();
136 let mut call_park_by_sid: HashMap<&str, Vec<&VoiceEvent>> = HashMap::new();
137
138 for event in &self.events {
140 if event.event_type == "call-park" {
141 if let Some(call_sid) = event.attributes.get("callSid") {
142 call_park_by_sid.entry(call_sid).or_insert_with(Vec::new).push(event);
143 }
144 }
145 }
146
147 for (_, events) in call_park_by_sid {
149 if let Some(latest_event) = events.iter().max_by_key(|e| e.timestamp) {
150 if latest_event.attributes.get("status").map(|s| s == "waiting").unwrap_or(false) {
151 parked_events.push(*latest_event);
152 }
153 }
154 }
155
156 parked_events
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct WhatsAppItem {
164 pub id: String,
165 pub phone_number_id: String,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub request: Option<WhatsappRequest>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub response: Option<WhatsappResponse>,
170 pub contacts: Vec<WhatsappContact>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub message: Option<WhatsappMessage>,
173 pub statuses: Vec<WhatsappStatus>,
174 pub timestamp: i64,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct WhatsappRequest {
179 pub to: String,
180 pub messaging_product: String,
181 #[serde(rename = "type")]
182 pub message_type: String,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub text: Option<WhatsappText>,
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub template: Option<WhatsappTemplate>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct WhatsappResponse {
191 pub messaging_product: String,
192 pub contacts: Vec<WhatsappContact>,
193 pub messages: Vec<WhatsappMessageId>,
194}
195
196impl WhatsappResponse {
197 pub fn get_first_message_id(&self) -> Option<String> {
198 self.messages.first().map(|m| m.id.clone())
199 }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct WhatsappContact {
204 pub input: String,
205 pub wa_id: String,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct WhatsappMessageId {
210 pub id: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct WhatsappMessage {
215 pub id: String,
216 pub from: String,
217 pub timestamp: String,
218 #[serde(rename = "type")]
219 pub message_type: String,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub text: Option<WhatsappText>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub image: Option<WhatsappMedia>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub video: Option<WhatsappMedia>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub audio: Option<WhatsappMedia>,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub document: Option<WhatsappDocument>,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub location: Option<WhatsappLocation>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct WhatsappText {
236 pub body: String,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct WhatsappTemplate {
241 pub name: String,
242 pub language: WhatsappLanguage,
243 pub components: Vec<serde_json::Value>,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct WhatsappLanguage {
248 pub code: String,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct WhatsappMedia {
253 pub id: String,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub mime_type: Option<String>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub sha256: Option<String>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub caption: Option<String>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct WhatsappDocument {
264 pub id: String,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub mime_type: Option<String>,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub sha256: Option<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub filename: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub caption: Option<String>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct WhatsappLocation {
277 pub latitude: f64,
278 pub longitude: f64,
279 #[serde(skip_serializing_if = "Option::is_none")]
280 pub name: Option<String>,
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub address: Option<String>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct WhatsappStatus {
287 pub id: String,
288 pub status: String, pub timestamp: String,
290 pub recipient_id: String,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub conversation: Option<WhatsappConversationContext>,
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub pricing: Option<WhatsappPricing>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct WhatsappConversationContext {
299 pub id: String,
300 pub origin: WhatsappConversationOrigin,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct WhatsappConversationOrigin {
305 #[serde(rename = "type")]
306 pub origin_type: String,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct WhatsappPricing {
311 pub billable: bool,
312 pub pricing_model: String,
313 pub category: String,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct ConversationNotification {
320 pub conversation_id: ConversationId,
321 #[serde(rename = "type")]
322 pub notification_type: String, #[serde(skip_serializing_if = "Option::is_none")]
324 pub item: Option<ConversationItem>,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub events: Option<Vec<VoiceEvent>>,
327 #[serde(skip_serializing_if = "Option::is_none")]
328 pub variables: Option<Vec<SessionVariable>>,
329 pub timestamp: i64,
330}
331
332impl ConversationNotification {
333 pub fn item(id: ConversationId, item: ConversationItem) -> Self {
334 Self {
335 conversation_id: id,
336 notification_type: "item".to_string(),
337 item: Some(item),
338 events: None,
339 variables: None,
340 timestamp: chrono::Utc::now().timestamp_millis(),
341 }
342 }
343
344 pub fn events(id: ConversationId, events: Vec<VoiceEvent>) -> Self {
345 Self {
346 conversation_id: id,
347 notification_type: "event".to_string(),
348 item: None,
349 events: Some(events),
350 variables: None,
351 timestamp: chrono::Utc::now().timestamp_millis(),
352 }
353 }
354
355 pub fn session(id: ConversationId, variables: Vec<SessionVariable>) -> Self {
356 Self {
357 conversation_id: id,
358 notification_type: "session".to_string(),
359 item: None,
360 events: None,
361 variables: Some(variables),
362 timestamp: chrono::Utc::now().timestamp_millis(),
363 }
364 }
365}
366
367impl Conversation {
370 pub fn within_whatsapp_period(&self) -> bool {
371 let twenty_four_hours_ago = chrono::Utc::now().timestamp_millis() - (24 * 60 * 60 * 1000);
372 self.items.iter()
373 .filter(|item| item.channel == "whatsapp")
374 .any(|item| item.timestamp > twenty_four_hours_ago)
375 }
376
377 pub fn last_message_phone_number_id(&self) -> Option<String> {
378 self.items.iter()
379 .filter(|item| item.channel == "whatsapp")
380 .filter_map(|item| item.whatsapp.as_ref())
381 .map(|w| w.phone_number_id.clone())
382 .next()
383 }
384
385 pub fn last_item(&self) -> Option<&ConversationItem> {
386 self.items.iter().max_by_key(|item| item.timestamp)
387 }
388
389 pub fn parked_calls(&self) -> Vec<&VoiceEvent> {
390 self.items.iter()
391 .filter_map(|item| item.voice.as_ref())
392 .flat_map(|voice| voice.parked_calls())
393 .collect()
394 }
395
396 pub fn has_parked_calls(&self) -> bool {
397 !self.parked_calls().is_empty()
398 }
399}
400