feat(api): add rest embed channel handler

This commit is contained in:
zhenyi 2026-05-31 13:12:28 +08:00
parent fcddc06cfe
commit 8b8c9e5e33

View File

@ -0,0 +1,91 @@
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}"))
}