cal_core/
conversation.rs

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