use anyhow::Result; use reqwest::Client; use serde::Deserialize; use super::ollama::OllamaClient; #[derive(Clone)] pub struct SmsApiClient { client: Client, base_url: String, token: Option, } impl SmsApiClient { pub fn new(base_url: String, token: Option) -> Self { Self { client: Client::new(), base_url, token, } } /// Fetch messages for a specific contact within ±1 day of the given timestamp /// Falls back to all contacts if no messages found for the specific contact /// Messages are sorted by proximity to the center timestamp pub async fn fetch_messages_for_contact( &self, contact: Option<&str>, center_timestamp: i64, ) -> Result> { use chrono::Duration; // Calculate ±2 days range around the center timestamp let center_dt = chrono::DateTime::from_timestamp(center_timestamp, 0) .ok_or_else(|| anyhow::anyhow!("Invalid timestamp"))?; let start_dt = center_dt - Duration::days(2); let end_dt = center_dt + Duration::days(2); let start_ts = start_dt.timestamp(); let end_ts = end_dt.timestamp(); // If contact specified, try fetching for that contact first if let Some(contact_name) = contact { log::info!( "Fetching SMS for contact: {} (±2 days from {})", contact_name, center_dt.format("%Y-%m-%d %H:%M:%S") ); let messages = self .fetch_messages(start_ts, end_ts, Some(contact_name), Some(center_timestamp)) .await?; if !messages.is_empty() { log::info!( "Found {} messages for contact {}", messages.len(), contact_name ); return Ok(messages); } log::info!( "No messages found for contact {}, falling back to all contacts", contact_name ); } // Fallback to all contacts log::info!( "Fetching all SMS messages (±1 day from {})", center_dt.format("%Y-%m-%d %H:%M:%S") ); self.fetch_messages(start_ts, end_ts, None, Some(center_timestamp)) .await } /// Fetch all messages for a specific contact across all time /// Used for embedding generation - retrieves complete message history /// Handles pagination automatically if the API returns a limited number of results pub async fn fetch_all_messages_for_contact(&self, contact: &str) -> Result> { let start_ts = chrono::DateTime::parse_from_rfc3339("2000-01-01T00:00:00Z") .unwrap() .timestamp(); let end_ts = chrono::Utc::now().timestamp(); log::info!("Fetching all historical messages for contact: {}", contact); let mut all_messages = Vec::new(); let mut offset = 0; let limit = 1000; // Fetch in batches of 1000 loop { log::debug!( "Fetching batch at offset {} for contact {}", offset, contact ); let batch = self .fetch_messages_paginated(start_ts, end_ts, Some(contact), None, limit, offset) .await?; let batch_size = batch.len(); all_messages.extend(batch); log::debug!( "Fetched {} messages (total so far: {})", batch_size, all_messages.len() ); // If we got fewer messages than the limit, we've reached the end if batch_size < limit { break; } offset += limit; } log::info!( "Fetched {} total messages for contact {}", all_messages.len(), contact ); Ok(all_messages) } /// Internal method to fetch messages with pagination support async fn fetch_messages_paginated( &self, start_ts: i64, end_ts: i64, contact: Option<&str>, center_timestamp: Option, limit: usize, offset: usize, ) -> Result> { let mut url = format!( "{}/api/messages/by-date-range/?start_date={}&end_date={}&limit={}&offset={}", self.base_url, start_ts, end_ts, limit, offset ); if let Some(contact_name) = contact { url.push_str(&format!("&contact={}", urlencoding::encode(contact_name))); } if let Some(ts) = center_timestamp { url.push_str(&format!("×tamp={}", ts)); } log::debug!("Fetching SMS messages from: {}", url); let mut request = self.client.get(&url); if let Some(token) = &self.token { request = request.header("Authorization", format!("Bearer {}", token)); } let response = request.send().await?; log::debug!("SMS API response status: {}", response.status()); if !response.status().is_success() { let status = response.status(); let error_body = response.text().await.unwrap_or_default(); log::error!("SMS API request failed: {} - {}", status, error_body); return Err(anyhow::anyhow!( "SMS API request failed: {} - {}", status, error_body )); } let data: SmsApiResponse = response.json().await?; Ok(data .messages .into_iter() .map(|m| SmsMessage { contact: m.contact_name, body: m.body, timestamp: m.date, is_sent: m.type_ == 2, }) .collect()) } /// Internal method to fetch messages with optional contact filter and timestamp sorting async fn fetch_messages( &self, start_ts: i64, end_ts: i64, contact: Option<&str>, center_timestamp: Option, ) -> Result> { // Call Django endpoint let mut url = format!( "{}/api/messages/by-date-range/?start_date={}&end_date={}", self.base_url, start_ts, end_ts ); // Add contact filter if provided if let Some(contact_name) = contact { url.push_str(&format!("&contact={}", urlencoding::encode(contact_name))); } // Add timestamp for proximity sorting if provided if let Some(ts) = center_timestamp { url.push_str(&format!("×tamp={}", ts)); } log::debug!("Fetching SMS messages from: {}", url); let mut request = self.client.get(&url); // Add authorization header if token exists if let Some(token) = &self.token { request = request.header("Authorization", format!("Bearer {}", token)); } let response = request.send().await?; log::debug!("SMS API response status: {}", response.status()); if !response.status().is_success() { let status = response.status(); let error_body = response.text().await.unwrap_or_default(); log::error!("SMS API request failed: {} - {}", status, error_body); return Err(anyhow::anyhow!( "SMS API request failed: {} - {}", status, error_body )); } let data: SmsApiResponse = response.json().await?; // Convert to internal format Ok(data .messages .into_iter() .map(|m| SmsMessage { contact: m.contact_name, body: m.body, timestamp: m.date, is_sent: m.type_ == 2, // type 2 = sent }) .collect()) } pub async fn summarize_context( &self, messages: &[SmsMessage], ollama: &OllamaClient, ) -> Result { if messages.is_empty() { return Ok(String::from("No messages on this day")); } // Create prompt for Ollama with sender/receiver distinction let messages_text: String = messages .iter() .take(60) // Limit to avoid token overflow .map(|m| { if m.is_sent { format!("Me: {}", m.body) } else { format!("{}: {}", m.contact, m.body) } }) .collect::>() .join("\n"); let prompt = format!( r#"Summarize these messages in up to 4-5 sentences. Focus on key topics, places, people mentioned, and the overall context of the conversations. Messages: {} Summary:"#, messages_text ); ollama .generate( &prompt, // Some("You are a summarizer for the purposes of jogging my memory and highlighting events and situations."), Some("You are the keeper of memories, ingest the context and give me a casual summary of the moment."), ) .await } } #[derive(Debug, Clone)] pub struct SmsMessage { pub contact: String, pub body: String, pub timestamp: i64, pub is_sent: bool, } #[derive(Deserialize)] struct SmsApiResponse { messages: Vec, } #[derive(Deserialize)] struct SmsApiMessage { contact_name: String, body: String, date: i64, #[serde(rename = "type")] type_: i32, }