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