use crate::{ContactRef, RecordReference};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[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, pub account: RecordReference,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationItem {
pub id: String,
pub channel: String, 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>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct SessionVariable {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceItem {
pub id: String,
pub direction: String, 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, pub timestamp: i64,
pub attributes: HashMap<String, String>,
}
impl VoiceItem {
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();
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);
}
}
}
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
}
}
#[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, 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,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationNotification {
pub conversation_id: ConversationId,
#[serde(rename = "type")]
pub notification_type: String, #[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(),
}
}
}
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()
}
}