cal-core 0.2.158

Callable core lib
Documentation
// File: cal-core/src/conversation.rs

use crate::{ContactRef, RecordReference};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};

// ==================== Main Conversation Structure ====================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conversation {
    pub id: ConversationId,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub contact: Option<ContactRef>,
    pub items: Vec<ConversationItem>,
    pub created: i64,
    pub last_updated: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ConversationId {
    pub from: String,  // E164 phone number
    pub account: RecordReference,
}


// ==================== Conversation Item ====================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationItem {
    pub id: String,
    pub channel: String, // "voice" or "whatsapp"
    pub timestamp: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub voice: Option<VoiceItem>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub whatsapp: Option<WhatsAppItem>,
    pub meta: HashSet<SessionVariable>,
}

// ==================== Session Variable ====================

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct SessionVariable {
    pub name: String,
    pub value: String,
}

// ==================== Voice Item ====================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceItem {
    pub id: String,
    pub direction: String, // "inbound" or "outbound"
    pub origin: String,
    pub from: String,
    pub to: String,
    pub call_sid: String,
    pub call_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub x_cid: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub diversion: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub p_asserted_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sbc_ip: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sbc_trunk_name: Option<String>,
    pub account_sid: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub application_sid: Option<String>,
    pub timestamp: i64,
    pub events: Vec<VoiceEvent>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceEvent {
    #[serde(rename = "type")]
    pub event_type: String, // "initial-request", "request", "event", "recording-event", "call-park"
    pub timestamp: i64,
    pub attributes: HashMap<String, String>,
}

impl VoiceItem {
    // Status check methods
    pub fn is_trying(&self) -> bool {
        self.events.iter()
            .filter(|e| e.event_type == "event")
            .max_by_key(|e| e.timestamp)
            .and_then(|e| e.attributes.get("sipStatus"))
            .and_then(|s| s.parse::<i32>().ok())
            .map(|status| status == 100)
            .unwrap_or(false)
    }

    pub fn is_ringing(&self) -> bool {
        self.events.iter()
            .filter(|e| e.event_type == "event")
            .max_by_key(|e| e.timestamp)
            .and_then(|e| e.attributes.get("sipStatus"))
            .and_then(|s| s.parse::<i32>().ok())
            .map(|status| status > 100 && status < 200)
            .unwrap_or(false)
    }

    pub fn is_connected(&self) -> bool {
        self.events.iter()
            .filter(|e| e.event_type == "event")
            .max_by_key(|e| e.timestamp)
            .and_then(|e| e.attributes.get("callStatus"))
            .map(|status| status == "in-progress")
            .unwrap_or(false)
    }

    pub fn is_completed(&self) -> bool {
        self.events.iter()
            .filter(|e| e.event_type == "event")
            .max_by_key(|e| e.timestamp)
            .and_then(|e| e.attributes.get("callStatus"))
            .map(|status| status == "completed")
            .unwrap_or(false)
    }

    pub fn is_recording(&self) -> bool {
        self.events.iter()
            .filter(|e| e.event_type == "recording-event")
            .filter(|e| e.attributes.get("type").map(|t| t == "dial").unwrap_or(false))
            .max_by_key(|e| e.timestamp)
            .and_then(|e| e.attributes.get("status"))
            .map(|status| status == "start")
            .unwrap_or(false)
    }

    pub fn parked_calls(&self) -> Vec<&VoiceEvent> {
        let mut parked_events = Vec::new();
        let mut call_park_by_sid: HashMap<&str, Vec<&VoiceEvent>> = HashMap::new();

        // Group call-park events by callSid
        for event in &self.events {
            if event.event_type == "call-park" {
                if let Some(call_sid) = event.attributes.get("callSid") {
                    call_park_by_sid.entry(call_sid).or_insert_with(Vec::new).push(event);
                }
            }
        }

        // Find events that are currently parked (status = "waiting")
        for (_, events) in call_park_by_sid {
            if let Some(latest_event) = events.iter().max_by_key(|e| e.timestamp) {
                if latest_event.attributes.get("status").map(|s| s == "waiting").unwrap_or(false) {
                    parked_events.push(*latest_event);
                }
            }
        }

        parked_events
    }
}

// ==================== WhatsApp Item ====================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsAppItem {
    pub id: String,
    pub phone_number_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request: Option<WhatsappRequest>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response: Option<WhatsappResponse>,
    pub contacts: Vec<WhatsappContact>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<WhatsappMessage>,
    pub statuses: Vec<WhatsappStatus>,
    pub timestamp: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappRequest {
    pub to: String,
    pub messaging_product: String,
    #[serde(rename = "type")]
    pub message_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<WhatsappText>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub template: Option<WhatsappTemplate>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappResponse {
    pub messaging_product: String,
    pub contacts: Vec<WhatsappContact>,
    pub messages: Vec<WhatsappMessageId>,
}

impl WhatsappResponse {
    pub fn get_first_message_id(&self) -> Option<String> {
        self.messages.first().map(|m| m.id.clone())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappContact {
    pub input: String,
    pub wa_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappMessageId {
    pub id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappMessage {
    pub id: String,
    pub from: String,
    pub timestamp: String,
    #[serde(rename = "type")]
    pub message_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<WhatsappText>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub image: Option<WhatsappMedia>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub video: Option<WhatsappMedia>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub audio: Option<WhatsappMedia>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub document: Option<WhatsappDocument>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub location: Option<WhatsappLocation>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappText {
    pub body: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappTemplate {
    pub name: String,
    pub language: WhatsappLanguage,
    pub components: Vec<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappLanguage {
    pub code: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappMedia {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mime_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sha256: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub caption: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappDocument {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mime_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sha256: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub filename: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub caption: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappLocation {
    pub latitude: f64,
    pub longitude: f64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub address: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappStatus {
    pub id: String,
    pub status: String, // "sent", "delivered", "read", "failed"
    pub timestamp: String,
    pub recipient_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conversation: Option<WhatsappConversationContext>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pricing: Option<WhatsappPricing>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappConversationContext {
    pub id: String,
    pub origin: WhatsappConversationOrigin,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappConversationOrigin {
    #[serde(rename = "type")]
    pub origin_type: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsappPricing {
    pub billable: bool,
    pub pricing_model: String,
    pub category: String,
}

// ==================== Conversation Notification ====================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationNotification {
    pub conversation_id: ConversationId,
    #[serde(rename = "type")]
    pub notification_type: String, // "item", "event", "session"
    #[serde(skip_serializing_if = "Option::is_none")]
    pub item: Option<ConversationItem>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub events: Option<Vec<VoiceEvent>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub variables: Option<Vec<SessionVariable>>,
    pub timestamp: i64,
}

impl ConversationNotification {
    pub fn item(id: ConversationId, item: ConversationItem) -> Self {
        Self {
            conversation_id: id,
            notification_type: "item".to_string(),
            item: Some(item),
            events: None,
            variables: None,
            timestamp: chrono::Utc::now().timestamp_millis(),
        }
    }

    pub fn events(id: ConversationId, events: Vec<VoiceEvent>) -> Self {
        Self {
            conversation_id: id,
            notification_type: "event".to_string(),
            item: None,
            events: Some(events),
            variables: None,
            timestamp: chrono::Utc::now().timestamp_millis(),
        }
    }

    pub fn session(id: ConversationId, variables: Vec<SessionVariable>) -> Self {
        Self {
            conversation_id: id,
            notification_type: "session".to_string(),
            item: None,
            events: None,
            variables: Some(variables),
            timestamp: chrono::Utc::now().timestamp_millis(),
        }
    }
}

// ==================== Helper Methods ====================

impl Conversation {
    pub fn within_whatsapp_period(&self) -> bool {
        let twenty_four_hours_ago = chrono::Utc::now().timestamp_millis() - (24 * 60 * 60 * 1000);
        self.items.iter()
            .filter(|item| item.channel == "whatsapp")
            .any(|item| item.timestamp > twenty_four_hours_ago)
    }

    pub fn last_message_phone_number_id(&self) -> Option<String> {
        self.items.iter()
            .filter(|item| item.channel == "whatsapp")
            .filter_map(|item| item.whatsapp.as_ref())
            .map(|w| w.phone_number_id.clone())
            .next()
    }

    pub fn last_item(&self) -> Option<&ConversationItem> {
        self.items.iter().max_by_key(|item| item.timestamp)
    }

    pub fn parked_calls(&self) -> Vec<&VoiceEvent> {
        self.items.iter()
            .filter_map(|item| item.voice.as_ref())
            .flat_map(|voice| voice.parked_calls())
            .collect()
    }

    pub fn has_parked_calls(&self) -> bool {
        !self.parked_calls().is_empty()
    }
}