refactor(site): carry site_id on Fetched; unify cache-key site lookup

This commit is contained in:
2026-08-14 18:17:22 +08:00
parent 96c11becb9
commit 7ca8fd1da2
5 changed files with 43 additions and 11 deletions
@@ -299,6 +299,7 @@ impl From<Post> for Fetched {
title: post.text.clone(), title: post.text.clone(),
media: post.media, media: post.media,
sensitive: post.sensitive, sensitive: post.sensitive,
site_id: "bsky",
render_data, render_data,
_keep_alive: None, _keep_alive: None,
} }
+35 -9
View File
@@ -30,6 +30,10 @@ pub struct Fetched {
pub media: Vec<crate::media::Media>, pub media: Vec<crate::media::Media>,
/// Spoiler flag for all media of this post. /// Spoiler flag for all media of this post.
pub sensitive: bool, pub sensitive: bool,
/// Site id (`"twitter"` / `"bsky"` / `"pixiv"`): the single source of
/// truth for site identity — caption-format lookup, cache-key prefix and
/// the SetFormat whitelist all derive from it. Set by the producing site.
pub site_id: &'static str,
/// Raw values (pre-escaped) for user-customizable caption formats. /// Raw values (pre-escaped) for user-customizable caption formats.
pub(crate) render_data: Option<RenderData>, pub(crate) render_data: Option<RenderData>,
/// Keeps temp files (e.g. an encoded ugoira MP4) alive until the caller /// Keeps temp files (e.g. an encoded ugoira MP4) alive until the caller
@@ -50,16 +54,10 @@ pub(crate) struct RenderData {
impl Fetched { impl Fetched {
/// The site this post came from (used for per-site format overrides). /// The site this post came from (used for per-site format overrides).
/// A thin alias over [`Fetched::site_id`] kept for callers that read the
/// site off a fetched post.
pub fn site_name(&self) -> &'static str { pub fn site_name(&self) -> &'static str {
if self.source_url.contains("x.com") || self.source_url.contains("twitter.com") { self.site_id
"twitter"
} else if self.source_url.contains("bsky.app") {
"bsky"
} else if self.source_url.contains("pixiv.net") {
"pixiv"
} else {
"unknown"
}
} }
/// Renders a user-supplied caption format. The format string is /// Renders a user-supplied caption format. The format string is
@@ -177,6 +175,25 @@ pub fn cache_key(url: &str) -> Option<String> {
None None
} }
/// The site id carried by a cache key (`"twitter:123"` → `"twitter"`).
/// Unknown prefixes fall back to `"unknown"`. The bot uses this on the
/// link-cache hit path, where no [`Fetched`] is available — the same value
/// a fresh fetch would read from [`Fetched::site_id`].
pub fn site_id_from_key(key: &str) -> &'static str {
match key.split(':').next() {
Some("twitter") => "twitter",
Some("pixiv") => "pixiv",
Some("bsky") => "bsky",
_ => "unknown",
}
}
/// Every supported site id, in dispatch order. The bot's SetFormat whitelist
/// and per-site caption-format lookup derive from this list.
pub fn site_ids() -> Vec<&'static str> {
vec!["twitter", "bsky", "pixiv"]
}
#[derive(Debug)] #[derive(Debug)]
pub enum FetchError { pub enum FetchError {
Http(reqwest::Error), Http(reqwest::Error),
@@ -476,6 +493,15 @@ mod tests {
assert_eq!(cache_key("https://example.com/not-a-post"), None); assert_eq!(cache_key("https://example.com/not-a-post"), None);
} }
#[test]
fn site_id_from_key_parses_prefix() {
assert_eq!(site_id_from_key("twitter:123"), "twitter");
assert_eq!(site_id_from_key("pixiv:123"), "pixiv");
assert_eq!(site_id_from_key("bsky:handle.example/3lorem"), "bsky");
assert_eq!(site_id_from_key("unknown:1"), "unknown");
assert_eq!(site_id_from_key("no-colon"), "unknown");
}
#[test] #[test]
fn fetch_error_retryability_classification() { fn fetch_error_retryability_classification() {
// Transient: network errors, explicit transient, pixiv 429/5xx. // Transient: network errors, explicit transient, pixiv 429/5xx.
@@ -142,6 +142,7 @@ impl From<Illustration> for Fetched {
title: illustration.title.clone(), title: illustration.title.clone(),
media: illustration.media, media: illustration.media,
sensitive: illustration.nsfw, sensitive: illustration.nsfw,
site_id: "pixiv",
render_data, render_data,
_keep_alive: illustration._keep_alive, _keep_alive: illustration._keep_alive,
} }
@@ -60,6 +60,7 @@ fn empty_fetched(url: &str) -> Fetched {
title: String::new(), title: String::new(),
media: vec![], media: vec![],
sensitive: true, sensitive: true,
site_id: "twitter",
render_data: None, render_data: None,
_keep_alive: None, _keep_alive: None,
} }
@@ -301,6 +302,7 @@ impl From<Tweet> for Fetched {
title: tweet.text.clone(), title: tweet.text.clone(),
media: tweet.media, media: tweet.media,
sensitive: tweet.sensitive, sensitive: tweet.sensitive,
site_id: "twitter",
render_data, render_data,
_keep_alive: None, _keep_alive: None,
} }
+4 -2
View File
@@ -402,7 +402,7 @@ async fn execute_command(
return Ok(()); return Ok(());
} }
}; };
if !["twitter", "bsky", "pixiv"].contains(&site) { if !x_media::site::site_ids().contains(&site) {
reply( reply(
bot.clone(), bot.clone(),
message.clone(), message.clone(),
@@ -636,7 +636,9 @@ async fn url_media(bot: Bot, message: &Message, url: &str) {
{ {
log::debug!("link cache hit for {key}"); log::debug!("link cache hit for {key}");
let chat_data = CHAT_STORE.get(chat_id).await; let chat_data = CHAT_STORE.get(chat_id).await;
let site = key.split(':').next().unwrap_or("unknown"); // Cache keys are prefixed with the site id ("twitter:…"), matching
// the value a fresh fetch would read from Fetched::site_id.
let site = x_media::site::site_id_from_key(&key);
let format = chat_data let format = chat_data
.message_format .message_format
.get(site) .get(site)