use std::time::Duration; use actix_web::{HttpResponse, web}; use serde::{Deserialize, Serialize}; use crate::channel::ChannelBus; const CACHE_KEY_PREFIX: &str = "oembed:twitter:"; const CACHE_TTL: Duration = Duration::from_secs(30 * 24 * 60 * 60); #[derive(Debug, Deserialize)] pub struct OEmbedQuery { pub url: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct OEmbedResponse { pub author_name: String, pub author_url: String, pub provider_name: String, pub html: String, pub width: Option, pub height: Option, } /// Proxy endpoint for fetching oEmbed data from Twitter/X. /// Responses are cached via AppCache for 30 days (tweets are immutable). pub async fn twitter_oembed( bus: web::Data, query: web::Query, ) -> Result { let tweet_url = &query.url; // Validate it's actually a Twitter/X URL if !tweet_url.starts_with("https://twitter.com/") && !tweet_url.starts_with("https://x.com/") && !tweet_url.starts_with("http://twitter.com/") && !tweet_url.starts_with("http://x.com/") { return Ok(HttpResponse::BadRequest().json(serde_json::json!({ "error": "URL must be a Twitter/X link" }))); } let cache = &bus.inner.cache; let cache_key = format!("{CACHE_KEY_PREFIX}{tweet_url}"); // Check cache first if let Ok(Some(cached)) = cache.get::(&cache_key).await { return Ok(HttpResponse::Ok().json(cached)); } let data = match fetch_oembed(tweet_url).await { Ok(data) => data, Err(err) => { return Ok(HttpResponse::InternalServerError() .json(serde_json::json!({ "error": err }))); } }; // Store in cache with 30-day TTL let _ = cache.set_with_ttl(&cache_key, &data, CACHE_TTL).await; Ok(HttpResponse::Ok().json(data)) } async fn fetch_oembed(tweet_url: &str) -> Result { let oembed_url = format!( "https://publish.twitter.com/oembed?url={}&omit_script=true&dnt=true", urlencoding::encode(tweet_url) ); let client = reqwest::Client::new(); let resp = client .get(&oembed_url) .header("User-Agent", "GitDataAI/1.0") .send() .await .map_err(|e| format!("Failed to fetch from Twitter: {e}"))?; if !resp.status().is_success() { return Err(format!( "Twitter returned status {}", resp.status().as_u16() )); } resp.json::() .await .map_err(|e| format!("Failed to parse oEmbed response: {e}")) }