//! oEmbed 抓取:把平台声明的端点返回的 JSON 收敛成 [`social::Enrichment`]。 //! //! 第三方返回的 HTML 不会被注入消息流:只从 `html` 里抠出 iframe 的 `src`,而且 //! `social` 侧还会再校验一次,只有落在该平台自己声明的域名里才会被采用。抓取失败 //! 一律返回 `None`,交给 OpenGraph 兜底,也不会写进 `chat_link_unfurl` 缓存。 use social::Enrichment; use crate::services::outbound::fetch_following; use crate::services::outbound::validate_outbound_url; const MAX_BYTES: usize = 262_144; /// 抓取 URL 所属平台的 oEmbed 数据;平台没声明端点或抓取失败时返回 `None`。 pub async fn fetch(url: &str) -> Option { let endpoint = social::oembed_endpoint(url)?; let body = fetch_following( &endpoint.endpoint, "application/json", Some("json"), MAX_BYTES, ) .await .ok()? .body; parse(&body) } /// 抓取页面自己声明的 oEmbed 端点。 /// /// 比 [`fetch`] 覆盖面大得多:注册表只硬编码了十几个平台,而页面里的 /// `` 是平台自己发布的, /// 新增平台不需要跟着改代码。 pub async fn fetch_endpoint(endpoint: &str) -> Option { if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") { return None; } if validate_outbound_url(endpoint).is_err() { return None; } let body = fetch_following(endpoint, "application/json", Some("json"), MAX_BYTES) .await .ok()? .body; parse(&body) } fn string_field(value: &serde_json::Value, key: &str) -> Option { let text = value.get(key)?.as_str()?.trim(); (!text.is_empty()).then(|| text.to_string()) } /// 从 oEmbed 的 `html` 字段里取出 iframe 的 `src`。 fn iframe_src(html: &str) -> Option { let start = html.to_ascii_lowercase().find(" Option { let value: serde_json::Value = serde_json::from_str(body).ok()?; if !value.is_object() { return None; } let enrichment = Enrichment { title: string_field(&value, "title"), description: string_field(&value, "description") .or_else(|| string_field(&value, "author_name")), image_url: string_field(&value, "thumbnail_url"), frame_src: string_field(&value, "html").as_deref().and_then(iframe_src), }; (enrichment.title.is_some() || enrichment.description.is_some() || enrichment.image_url.is_some() || enrichment.frame_src.is_some()) .then_some(enrichment) } #[cfg(test)] mod tests { use super::*; #[test] fn parses_the_youtube_payload() { let body = r#"{"title":"Rick Astley - Never Gonna Give You Up (Official Video)","author_name":"Rick Astley","author_url":"https://www.youtube.com/@RickAstleyYT","thumbnail_url":"https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg","type":"video","version":"1.0"}"#; let enrichment = parse(body).unwrap(); assert_eq!( enrichment.title.as_deref(), Some("Rick Astley - Never Gonna Give You Up (Official Video)") ); assert_eq!(enrichment.description.as_deref(), Some("Rick Astley")); assert_eq!( enrichment.image_url.as_deref(), Some("https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg") ); assert!(enrichment.frame_src.is_none()); } #[test] fn pulls_the_frame_out_of_the_spotify_payload() { let body = r#"{"title":"Never Gonna Give You Up","html":""}"#; let enrichment = parse(body).unwrap(); assert_eq!( enrichment.frame_src.as_deref(), Some("https://open.spotify.com/embed/track/4cOdK2wGLETKBW3PvgPWqT?utm_source=oembed") ); } #[test] fn rejects_bodies_without_useful_fields() { assert!(parse("nope").is_none()); assert!(parse("{}").is_none()); assert!(parse(r#"{"type":"video","html":"
quote
"}"#).is_none()); } }