cal_core/
conversation.rs

1// File: cal-core/src/conversation.rs
2
3use crate::{ContactRef, RecordReference};
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, HashSet};
6
7// ==================== Main Conversation Structure ====================
8
9#[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,  // E164 phone number
22    pub account: RecordReference,
23}
24
25
26// ==================== Conversation Item ====================
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ConversationItem {
30    pub id: String,
31    pub channel: String, // "voice" or "whatsapp"
32    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// ==================== Session Variable ====================
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
43pub struct SessionVariable {
44    pub name: String,
45    pub value: String,
46}
47
48// ==================== Voice Item ====================
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct VoiceItem {
52    pub id: String,
53    pub direction: String, // "inbound" or "outbound"
54    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, // "initial-request", "request", "event", "recording-event", "call-park"
80    pub timestamp: i64,
81    pub attributes: HashMap<String, String>,
82}
83
84impl VoiceItem {
85    // Status check methods
86    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        // Group call-park events by callSid
139        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        // Find events that are currently parked (status = "waiting")
148        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// ==================== WhatsApp Item ====================
161
162#[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, // "sent", "delivered", "read", "failed"
289    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// ==================== Conversation Notification ====================
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct ConversationNotification {
320    pub conversation_id: ConversationId,
321    #[serde(rename = "type")]
322    pub notification_type: String, // "item", "event", "session"
323    #[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
367// ==================== Helper Methods ====================
368
369impl 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