// Google People API client extracted from external_sync.rs use crate::contacts::external_sync::{ExternalContact, ExternalEmail, ExternalPhone}; use chrono::{DateTime, Utc}; use reqwest::Client; use serde::Deserialize; use std::collections::HashMap; #[derive(Debug, Clone)] pub struct GoogleClient { pub client: Client, pub base_url: String, } #[derive(Debug, Clone)] pub struct GoogleConfig { pub client_id: String, pub client_secret: String, } pub struct GoogleContactsClient { config: GoogleConfig, client: Client, } impl GoogleContactsClient { pub fn new(config: GoogleConfig) -> Self { Self { config, client: Client::new(), } } pub fn get_auth_url(&self, redirect_uri: &str, state: &str) -> String { format!( "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri={}&response_type=code&scope=https://www.googleapis.com/auth/contacts&state={}", self.config.client_id, redirect_uri, state ) } pub async fn exchange_code(&self, code: &str, redirect_uri: &str) -> Result { let response = self.client .post("https://oauth2.googleapis.com/token") .form(&[ ("client_id", self.config.client_id.as_str()), ("client_secret", self.config.client_secret.as_str()), ("code", code), ("redirect_uri", redirect_uri), ("grant_type", "authorization_code"), ]) .send() .await .map_err(|e| GoogleError::NetworkError(e.to_string()))?; if !response.status().is_success() { return Err(GoogleError::ApiError(format!("Token exchange failed: {}", response.status()))); } #[derive(Deserialize)] struct GoogleTokenResponse { access_token: String, refresh_token: Option, expires_in: i64, scope: Option, } let token_data: GoogleTokenResponse = response.json().await .map_err(|e| GoogleError::ParseError(e.to_string()))?; Ok(TokenResponse { access_token: token_data.access_token, refresh_token: token_data.refresh_token, expires_in: token_data.expires_in, expires_at: Some(Utc::now() + chrono::Duration::seconds(token_data.expires_in)), scopes: token_data.scope.map(|s| s.split(' ').map(String::from).collect()).unwrap_or_default(), }) } pub async fn get_user_info(&self, access_token: &str) -> Result { let response = self.client .get("https://www.googleapis.com/oauth2/v2/userinfo") .bearer_auth(access_token) .send() .await .map_err(|e| GoogleError::NetworkError(e.to_string()))?; if !response.status().is_success() { return Err(GoogleError::ApiError("Failed to get user info".to_string())); } #[derive(Deserialize)] struct GoogleUserInfo { id: String, email: String, name: Option, } let info: GoogleUserInfo = response.json().await .map_err(|e| GoogleError::ParseError(e.to_string()))?; Ok(UserInfo { id: info.id, email: info.email, name: info.name, }) } pub async fn revoke_token(&self, _access_token: &str) -> Result<(), GoogleError> { // Simple revoke - in real implementation would call revoke endpoint Ok(()) } } #[derive(Debug, Clone)] pub struct UserInfo { pub id: String, pub email: String, pub name: Option, } #[derive(Debug, Clone)] pub struct TokenResponse { pub access_token: String, pub refresh_token: Option, pub expires_in: i64, pub expires_at: Option>, pub scopes: Vec, } impl GoogleClient { pub fn new() -> Self { Self { client: Client::new(), base_url: "https://people.googleapis.com/v1".to_string(), } } pub async fn fetch_contacts(&self, access_token: &str) -> Result<(Vec, Option), GoogleError> { let mut all_contacts = Vec::new(); let mut page_token: Option = None; loop { let (contacts, next_token) = self.list_contacts(access_token, page_token.as_deref()).await?; all_contacts.extend(contacts); if next_token.is_none() { break; } page_token = next_token; if all_contacts.len() > 10000 { log::warn!("Reached contact fetch limit"); break; } } Ok((all_contacts, None)) } pub async fn list_contacts( &self, access_token: &str, page_token: Option<&str>, ) -> Result<(Vec, Option), GoogleError> { let mut url = format!( "{}/people/me/connections?personFields=names,emailAddresses,phoneNumbers,organizations,biographies", self.base_url ); if let Some(token) = page_token { url.push_str(&format!("&pageToken={}", token)); } let response = self .client .get(&url) .bearer_auth(access_token) .send() .await .map_err(|e| GoogleError::NetworkError(e.to_string()))?; if !response.status().is_success() { return Err(GoogleError::ApiError(format!( "Failed to list contacts: {}", response.status() ))); } #[derive(Deserialize)] struct GoogleResponse { connections: Option>, next_page_token: Option, } let data: GoogleResponse = response .json() .await .map_err(|e| GoogleError::ParseError(e.to_string()))?; let contacts = data .connections .unwrap_or_default() .into_iter() .map(|person| { let first_name = person .names .as_ref() .and_then(|n| n.first().map(|n| n.given_name.clone())) .unwrap_or_default(); let last_name = person .names .as_ref() .and_then(|n| n.first().map(|n| n.family_name.clone())) .unwrap_or_default(); let display_name = person .names .as_ref() .and_then(|n| n.first().and_then(|n| n.display_name.clone())) .unwrap_or_default(); let email = person.email_addresses.as_ref().and_then(|emails| { emails .first() .and_then(|e| e.value.clone()) .map(|addr| ExternalEmail { address: addr, label: e.metadata.as_ref().and_then(|m| m.primary.clone()), primary: e.metadata.as_ref().map(|m| m.primary).unwrap_or(false), }) }); let phone = person.phone_numbers.as_ref().and_then(|phones| { phones.first().map(|p| ExternalPhone { number: p.value.clone().unwrap_or_default(), label: p.metadata.as_ref().and_then(|m| m.primary.clone()), primary: p.metadata.as_ref().map(|m| m.primary).unwrap_or(false), }) }); ExternalContact { id: person.resource_name.unwrap_or_default(), etag: person.etag, first_name, last_name, display_name, email_addresses: email.map(|e| vec![e]).unwrap_or_default(), phone_numbers: phone.map(|p| vec![p]).unwrap_or_default(), addresses: vec![], company: person .organizations .as_ref() .and_then(|o| o.first().and_then(|org| org.name.clone())), job_title: person .organizations .as_ref() .and_then(|o| o.first().and_then(|org| org.title.clone())), department: None, notes: person.biographies.as_ref().and_then(|b| { b.first() .and_then(|bio| bio.content.clone()) .map(|c| c.clone()) }), birthday: None, photo_url: person.photos.as_ref().and_then(|photos| { photos.first().and_then(|photo| photo.url.clone()) }), groups: vec![], custom_fields: Default::default(), created_at: None, updated_at: None, } }) .collect(); Ok((contacts, data.next_page_token)) } pub async fn create_contact( &self, access_token: &str, contact: &ExternalContact, ) -> Result { let body = serde_json::json!({ "names": [{ "givenName": contact.first_name, "familyName": contact.last_name, "displayName": contact.display_name }], "emailAddresses": if contact.email_addresses.is_empty() { None } else { Some(contact.email_addresses.iter().map(|e| serde_json::json!({ "value": e.address, "metadata": {"primary": e.primary} })).collect::>()) }, "phoneNumbers": if contact.phone_numbers.is_empty() { None } else { Some(contact.phone_numbers.iter().map(|p| serde_json::json!({ "value": p.number, "metadata": {"primary": p.primary} })).collect::>()) }, "organizations": if contact.company.is_some() || contact.job_title.is_some() { Some(vec![serde_json::json!({ "name": contact.company.unwrap_or_default(), "title": contact.job_title.unwrap_or_default() })]) } else { None } }); let response = self .client .post(&format!( "{}/people/me/connections:create", self.base_url )) .query(&[("personFields", "names,emailAddresses,phoneNumbers,organizations")]) .bearer_auth(access_token) .json(&body) .send() .await .map_err(|e| GoogleError::NetworkError(e.to_string()))?; if !response.status().is_success() { return Err(GoogleError::ApiError(format!( "Create contact failed: {}", response.status() ))); } #[derive(Deserialize)] struct CreateResponse { resourceName: String, } let data: CreateResponse = response .json() .await .map_err(|e| GoogleError::ParseError(e.to_string()))?; Ok(data.resourceName) } pub async fn update_contact( &self, access_token: &str, resource_name: &str, contact: &ExternalContact, ) -> Result<(), GoogleError> { let body = serde_json::json!({ "names": [{ "givenName": contact.first_name, "familyName": contact.last_name, "displayName": contact.display_name }], "emailAddresses": if contact.email_addresses.is_empty() { None } else { Some(contact.email_addresses.iter().map(|e| serde_json::json!({ "value": e.address, "metadata": {"primary": e.primary} })).collect::>()) }, "phoneNumbers": if contact.phone_numbers.is_empty() { None } else { Some(contact.phone_numbers.iter().map(|p| serde_json::json!({ "value": p.number, "metadata": {"primary": p.primary} })).collect::>()) }, "organizations": if contact.company.is_some() || contact.job_title.is_some() { Some(vec![serde_json::json!({ "name": contact.company.unwrap_or_default(), "title": contact.job_title.unwrap_or_default() })]) } else { None } }); let response = self .client .patch(&format!( "{}/people/me/{}:update", self.base_url, resource_name )) .query(&[("personFields", "names,emailAddresses,phoneNumbers,organizations")]) .bearer_auth(access_token) .json(&body) .send() .await .map_err(|e| GoogleError::NetworkError(e.to_string()))?; if !response.status().is_success() { return Err(GoogleError::ApiError(format!( "Update contact failed: {}", response.status() ))); } Ok(()) } pub async fn delete_contact( &self, access_token: &str, resource_name: &str, ) -> Result<(), GoogleError> { let response = self .client .delete(&format!( "{}/people/me/{}", self.base_url, resource_name )) .bearer_auth(access_token) .send() .await .map_err(|e| GoogleError::NetworkError(e.to_string()))?; if !response.status().is_success() { return Err(GoogleError::ApiError(format!( "Delete contact failed: {}", response.status() ))); } Ok(()) } } #[derive(Debug, Clone)] pub enum GoogleError { NetworkError(String), ApiError(String), ParseError(String), } impl std::fmt::Display for GoogleError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::NetworkError(e) => write!(f, "Network error: {e}"), Self::ApiError(e) => write!(f, "API error: {e}"), Self::ParseError(e) => write!(f, "Parse error: {e}"), } } } impl std::error::Error for GoogleError {} #[derive(Debug, Clone, Deserialize)] struct GooglePerson { resource_name: Option, etag: Option, names: Option>, email_addresses: Option>, phone_numbers: Option>, organizations: Option>, biographies: Option>, photos: Option>, } #[derive(Debug, Clone, Deserialize)] struct GoogleName { given_name: String, family_name: String, display_name: Option, metadata: Option, } #[derive(Debug, Clone, Deserialize)] struct GoogleEmail { value: String, metadata: Option, } #[derive(Debug, Clone, Deserialize)] struct GooglePhone { value: Option, metadata: Option, } #[derive(Debug, Clone, Deserialize)] struct GoogleOrganization { name: Option, title: Option, } #[derive(Debug, Clone, Deserialize)] struct GoogleBiography { content: Option, } #[derive(Debug, Clone, Deserialize)] struct GooglePhoto { url: Option, } #[derive(Debug, Clone, Deserialize)] struct GoogleMetadata { primary: Option, }