92 lines
2.6 KiB
Rust
92 lines
2.6 KiB
Rust
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<i32>,
|
|
pub height: Option<i32>,
|
|
}
|
|
|
|
/// 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<ChannelBus>,
|
|
query: web::Query<OEmbedQuery>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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::<OEmbedResponse>(&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<OEmbedResponse, String> {
|
|
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::<OEmbedResponse>()
|
|
.await
|
|
.map_err(|e| format!("Failed to parse oEmbed response: {e}"))
|
|
}
|