main
//! URL → 平台匹配与 embed 解析。
//!
//! 平台定义与域名路由在 [`crate::parse`];这里只做入口校验、直接媒体兜底,
//! 以及对外的 [`parse`] / [`embed_json`] 包装。
use url::Url;
use crate::parse::OembedEndpoint;
use crate::{Aspect, Category, Embed, EmbedKind, Enrichment, Resolved};
fn direct_media(url: &Url) -> Option<(&'static str, &'static str, EmbedKind)> {
let path = url.path().to_ascii_lowercase();
let ext = path.rsplit('.').next().unwrap_or_default();
let kind = match ext {
"mp4" | "webm" | "mov" | "m4v" | "ogv" => EmbedKind::Video,
"mp3" | "m4a" | "aac" | "flac" | "wav" | "ogg" | "opus" => EmbedKind::Audio,
"jpg" | "jpeg" | "png" | "gif" | "webp" | "avif" | "bmp" | "svg" => EmbedKind::Image,
_ => return None,
};
let (id, name) = match kind {
EmbedKind::Image => ("web-image", "图片"),
EmbedKind::Audio => ("web-audio", "音频"),
_ => ("web-video", "视频"),
};
Some((id, name, kind))
}
/// 解析一个 URL。非 http(s)、无 host、或不在注册表里(且不是直接媒体链接)时返回 `None`。
pub fn parse(raw: &str) -> Option<Resolved> {
let trimmed = raw.trim();
if trimmed.len() > 4096 {
return None;
}
let url = Url::parse(trimmed).ok()?;
if !matches!(url.scheme(), "http" | "https") {
return None;
}
url.host_str()?;
if let Some(platform) = crate::parse::route(&url) {
return platform.parse(&url);
}
let (id, name, kind) = direct_media(&url)?;
let aspect = match kind {
EmbedKind::Video => Aspect::Wide,
_ => Aspect::Auto,
};
Some(Resolved {
platform: id,
platform_name: name,
category: Category::Other,
kind,
url: url.to_string(),
src: Some(url.to_string()),
aspect,
height: None,
title: name.to_string(),
description: None,
image_url: None,
})
}
/// chat 消息 `embeds` 数组里的一项(`type = "platform"`)。
pub fn embed_json(raw: &str) -> Option<serde_json::Value> {
let resolved = parse(raw)?;
let html = crate::render_html(&resolved);
Some(Embed { resolved, html }.to_json())
}
/// URL 命中的平台声明的 oEmbed 端点(未声明时返回 `None`)。
pub fn oembed_endpoint(raw: &str) -> Option<OembedEndpoint> {
let url = Url::parse(raw.trim()).ok()?;
if !matches!(url.scheme(), "http" | "https") {
return None;
}
crate::parse::oembed_endpoint(&url)
}
fn absolute_http_url(value: &str) -> Option<&str> {
let trimmed = value.trim();
if trimmed.is_empty() || trimmed.len() > 2048 {
return None;
}
let url = Url::parse(trimmed).ok()?;
matches!(url.scheme(), "http" | "https").then_some(trimmed)
}
/// oEmbed 返回的 iframe 地址只有在平台自己声明的域名下才采用,
/// 否则第三方页面会被嵌进消息流。
fn owned_frame_src(raw: &str, url: &Url) -> Option<String> {
let candidate = absolute_http_url(raw)?;
let host = Url::parse(candidate)
.ok()?
.host_str()?
.trim_end_matches('.')
.to_ascii_lowercase();
let platform = crate::parse::route(url)?;
platform
.domains()
.iter()
.any(|domain| {
let domain = domain.trim_start_matches('.').to_ascii_lowercase();
host == domain || host.ends_with(&format!(".{domain}"))
})
.then(|| candidate.to_string())
}
/// 解析 + 合并联网补全(标题 / 简介 / 封面 / iframe)+ 渲染。
pub fn embed_json_enriched(raw: &str, enrichment: &Enrichment) -> Option<serde_json::Value> {
let trimmed = raw.trim();
let mut resolved = parse(trimmed)?;
let url = Url::parse(trimmed).ok()?;
{
let resolved = &mut resolved;
let url = &url;
if let Some(title) = enrichment
.title
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
resolved.title = title.chars().take(200).collect();
}
if let Some(description) = enrichment
.description
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
resolved.description = Some(description.chars().take(300).collect());
}
if let Some(image) = enrichment.image_url.as_deref().and_then(absolute_http_url) {
resolved.image_url = Some(image.to_string());
}
if resolved.kind == EmbedKind::Link
&& let Some(frame) = enrichment
.frame_src
.as_deref()
.and_then(|raw| owned_frame_src(raw, url))
{
resolved.kind = EmbedKind::Iframe;
resolved.src = Some(frame);
resolved.aspect = Aspect::Wide;
}
}
let html = crate::render_html(&resolved);
Some(Embed { resolved, html }.to_json())
}
#[cfg(test)]
mod tests {
use super::*;
fn kind_of(url: &str) -> Option<(EmbedKind, Option<String>, &'static str)> {
parse(url).map(|resolved| (resolved.kind, resolved.src, resolved.platform))
}
#[test]
fn resolves_the_named_platforms() {
let cases: &[(&str, &str)] = &[
("https://mp.weixin.qq.com/s/AbCdEfGhIjKlMn", "wechat"),
("https://www.douyin.com/video/7412345678901234567", "douyin"),
("https://weibo.com/1234567890/AbCdEfGh", "weibo"),
("https://www.bilibili.com/video/BV1xx411c7mD", "bilibili"),
("https://www.zhihu.com/question/12345678", "zhihu"),
("https://item.taobao.com/item.htm?id=123456", "taobao"),
("https://item.jd.com/100012043978.html", "jd"),
("https://www.meituan.com/meishi/123456/", "meituan"),
("https://www.alipay.com/", "alipay"),
("https://www.dingtalk.com/", "dingtalk"),
("https://www.feishu.cn/", "feishu"),
("https://www.baidu.com/s?wd=gitdata", "baidu"),
("https://music.163.com/#/song?id=1234567", "netease-music"),
("https://www.iqiyi.com/v_1abcdefg.html", "iqiyi"),
("https://www.facebook.com/permalink.php", "facebook"),
("https://www.instagram.com/p/Cabcdefghij/", "instagram"),
("https://www.youtube.com/watch?v=dQw4w9WgXcQ", "youtube"),
(
"https://www.tiktok.com/@user/video/7412345678901234567",
"tiktok",
),
("https://x.com/user/status/1234567890123456789", "twitter"),
("https://www.netflix.com/title/80100172", "netflix"),
];
for (url, expected) in cases {
let resolved = parse(url).unwrap_or_else(|| panic!("{url} did not resolve"));
assert_eq!(resolved.platform, *expected, "{url}");
}
}
/// 本轮抽检修掉的回归点,逐条钉住,避免以后静默退化。
#[test]
fn regression_cases_from_the_platform_sweep() {
let src_of = |url: &str| {
let resolved = parse(url).unwrap_or_else(|| panic!("{url} did not resolve"));
(
resolved.platform,
resolved.kind,
resolved.src.unwrap_or_default(),
)
};
// Twitch:频道直播此前只认 `/videos/<id>`,频道页退化成卡片。
let (platform, kind, src) = src_of("https://www.twitch.tv/xqc");
assert_eq!(platform, "twitch");
assert_eq!(kind, EmbedKind::Iframe);
assert!(src.contains("channel=xqc"), "{src}");
// 目录页不能被当成主播。
assert_eq!(src_of("https://www.twitch.tv/directory").1, EmbedKind::Link);
// B 站:旧版 av 号与番剧 ep 号各自拼对播放器参数。
let (_, _, src) = src_of("https://www.bilibili.com/video/av1134025");
assert!(src.contains("aid=1134025"), "{src}");
let (_, _, src) = src_of("https://www.bilibili.com/bangumi/play/ep123456");
assert!(src.contains("ep_id=123456"), "{src}");
// YouTube 播放列表走 videoseries,而不是 embed/{id}。
let (_, kind, src) = src_of("https://www.youtube.com/playlist?list=PLMC9KNkIncKtPzgY");
assert_eq!(kind, EmbedKind::Iframe);
assert!(src.contains("videoseries?list=PLMC9KNkIncKtPzgY"), "{src}");
// JSFiddle:`/<user>/<id>` 不能把用户名当 id,路径要原样保留。
let (_, kind, src) = src_of("https://jsfiddle.net/zalun/Nmud8/");
assert_eq!(kind, EmbedKind::Iframe);
assert_eq!(
src,
"https://jsfiddle.net/zalun/Nmud8/embedded/js,html,css,result/dark/"
);
let (_, kind, src) = src_of("https://jsfiddle.net/6he52mnr/1/");
assert_eq!(kind, EmbedKind::Iframe);
assert!(
src.starts_with("https://jsfiddle.net/6he52mnr/1/embedded/"),
"{src}"
);
// QQ 音乐:歌曲页用官方外链播放器。
let (_, kind, src) = src_of("https://y.qq.com/n/ryqq/songDetail/0039MnYb0qxYhV");
assert_eq!(kind, EmbedKind::Iframe);
assert!(src.contains("songid=0039MnYb0qxYhV"), "{src}");
// 歌单/歌手没有公开播放器,保持卡片。
assert_eq!(
src_of("https://y.qq.com/n/ryqq/playlist/1234567890").1,
EmbedKind::Link
);
// 喜马拉雅:声音页与专辑页各用对应播放器。
let (_, kind, src) = src_of("https://www.ximalaya.com/sound/123456789");
assert_eq!(kind, EmbedKind::Iframe);
assert!(src.contains("/sound/player.html?id=123456789"), "{src}");
let (_, _, src) = src_of("https://www.ximalaya.com/album/12345678");
assert!(src.contains("/album/player.html?id=12345678"), "{src}");
// Audius:`/<artist>/<track>` -> `/embed/<artist>/<track>`。
let (_, kind, src) = src_of("https://audius.co/deadmau5/strobe");
assert_eq!(kind, EmbedKind::Iframe);
assert_eq!(src, "https://audius.co/embed/deadmau5/strobe");
// Google Maps:不能再把整个 URL 塞进 `q=`。
let (_, _, src) = src_of("https://maps.google.com/maps?q=tokyo");
assert_eq!(src, "https://maps.google.com/maps?q=tokyo&output=embed");
let (_, _, src) = src_of("https://www.google.com/maps/place/Tokyo");
assert_eq!(src, "https://maps.google.com/maps?q=Tokyo&output=embed");
// 非地图的 google.com 链接不能被认领。
assert!(parse("https://www.google.com/search?q=rust").is_none());
}
#[test]
fn builds_youtube_iframe_and_falls_back_to_card() {
let (kind, src, _) = kind_of("https://youtu.be/dQw4w9WgXcQ").unwrap();
assert_eq!(kind, EmbedKind::Iframe);
assert_eq!(
src.as_deref(),
Some("https://www.youtube.com/embed/dQw4w9WgXcQ")
);
let (kind, src, _) = kind_of("https://www.youtube.com/results?search_query=rust").unwrap();
assert_eq!(kind, EmbedKind::Link);
assert!(src.is_none());
}
#[test]
fn spotify_uses_two_captures() {
let (kind, src, _) =
kind_of("https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT").unwrap();
assert_eq!(kind, EmbedKind::Iframe);
assert_eq!(
src.as_deref(),
Some("https://open.spotify.com/embed/track/4cOdK2wGLETKBW3PvgPWqT")
);
}
#[test]
fn direct_media_is_embedded() {
let (kind, src, platform) = kind_of("https://cdn.example.com/clip.mp4").unwrap();
assert_eq!(kind, EmbedKind::Video);
assert_eq!(platform, "web-video");
assert_eq!(src.as_deref(), Some("https://cdn.example.com/clip.mp4"));
}
#[test]
fn rejects_non_http() {
assert!(parse("javascript:alert(1)").is_none());
assert!(parse("file:///etc/passwd").is_none());
assert!(parse("not a url").is_none());
}
#[test]
fn oembed_endpoint_follows_the_declared_platform() {
let endpoint =
oembed_endpoint("https://www.youtube.com/watch?v=dQw4w9WgXcQ").expect("youtube");
assert_eq!(endpoint.platform, "youtube");
assert!(
endpoint
.endpoint
.starts_with("https://www.youtube.com/oembed")
);
assert!(endpoint.endpoint.contains("dQw4w9WgXcQ"));
assert!(!endpoint.endpoint.contains('{'));
assert!(oembed_endpoint("https://example.com/post").is_none());
}
#[test]
fn enrichment_fills_the_card_but_never_foreign_frames() {
let url = "https://mp.weixin.qq.com/s/AbCdEfGhIjKlMn";
let enrichment = Enrichment {
title: Some(" 文章标题 ".into()),
description: Some("摘要".into()),
image_url: Some("https://mmbiz.qpic.cn/cover.jpg".into()),
frame_src: Some("https://evil.example/frame".into()),
};
let value = embed_json_enriched(url, &enrichment).unwrap();
assert_eq!(value["kind"], "link");
assert_eq!(value["title"], "文章标题");
assert_eq!(value["description"], "摘要");
assert_eq!(value["image_url"], "https://mmbiz.qpic.cn/cover.jpg");
let html = value["html"].as_str().unwrap();
assert!(html.contains("gd-embed-thumb"), "{html}");
assert!(html.contains("摘要"), "{html}");
assert!(!html.contains("evil.example"), "{html}");
}
#[test]
fn owned_frames_upgrade_a_card_and_unsafe_urls_are_dropped() {
let url = "https://mp.weixin.qq.com/s/AbCdEfGhIjKlMn";
let enrichment = Enrichment {
frame_src: Some("https://mp.weixin.qq.com/mp/embed?x=1".into()),
..Enrichment::default()
};
let value = embed_json_enriched(url, &enrichment).unwrap();
assert_eq!(value["kind"], "iframe");
assert_eq!(value["src"], "https://mp.weixin.qq.com/mp/embed?x=1");
assert!(value["html"].as_str().unwrap().contains("<iframe"));
let unsafe_image = Enrichment {
image_url: Some("javascript:alert(1)".into()),
..Enrichment::default()
};
let value = embed_json_enriched(url, &unsafe_image).unwrap();
assert!(value["image_url"].is_null());
assert_eq!(value["kind"], "link");
}
}