feat(api): add rest embed channel handler
This commit is contained in:
parent
fcddc06cfe
commit
8b8c9e5e33
91
lib/api/src/channel/rest_embed.rs
Normal file
91
lib/api/src/channel/rest_embed.rs
Normal 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}"))
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user