mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-09-23 23:32:05 +00:00
perf: parse a twitter syndication body once
`fetch` classified the response with `parse_syndication_body` — which parses the whole body into a `serde_json::Value` — and then threw that value away and re-parsed the same text into a `SyndicationTweet`. Two full JSON scans and two allocations of every string in the body (text, entities, media details) per tweet. `Tweet::from_syndication_value` takes the value classification already built and deserializes from that: `from_value` moves the strings out of the tree instead of allocating copies, so the response text is scanned once. The auth fallback had the mirror image of the same waste — it built the syndication shape as a `Value` and then serialized it back to a string for a parse — and the 8 test call sites lose their `.to_string()` with it. Verified live: `cargo test -p x-media -- --ignored live` (14 passed), including the 5 twitter fetches that exercise this path. `cargo fmt --check`, `cargo clippy --workspace --all-targets --locked -- -D warnings` and `cargo test --workspace --locked` clean.
This commit is contained in:
@@ -149,7 +149,7 @@ pub async fn fetch(id: &str) -> Result<Tweet, FetchError> {
|
||||
"missing tweet fields in GraphQL response",
|
||||
)))
|
||||
})?;
|
||||
Tweet::from_syndication_json(&syndication_shape.to_string()).map_err(FetchError::Json)
|
||||
Tweet::from_syndication_value(syndication_shape).map_err(FetchError::Json)
|
||||
}
|
||||
|
||||
/// Locates the tweet for `id` in a `TweetDetail` response and unwraps
|
||||
@@ -223,7 +223,7 @@ fn normalize_tweet_result(result: &Value) -> Result<Value, FetchError> {
|
||||
}
|
||||
|
||||
/// Maps a GraphQL `{core, legacy, ...}` tweet onto the syndication JSON
|
||||
/// shape [`Tweet::from_syndication_json`] parses, so the existing text /
|
||||
/// shape [`Tweet::from_syndication_value`] parses, so the existing text /
|
||||
/// media handling (t.co expansion, `name=orig`, mp4 variant) is reused.
|
||||
fn to_syndication_shape(tweet: &Value) -> Option<Value> {
|
||||
let legacy = tweet.get("legacy")?;
|
||||
@@ -308,7 +308,7 @@ mod tests {
|
||||
let json = conversation(tweet_result());
|
||||
let result = parse_tweet_result(&json, "2083868672721039569").unwrap();
|
||||
let shape = to_syndication_shape(&result).unwrap();
|
||||
let tweet = Tweet::from_syndication_json(&shape.to_string()).unwrap();
|
||||
let tweet = Tweet::from_syndication_value(shape).unwrap();
|
||||
let fetched: crate::site::Fetched = tweet.into();
|
||||
|
||||
assert!(fetched.sensitive);
|
||||
|
||||
@@ -112,13 +112,16 @@ pub async fn fetch(id: &str) -> Result<Tweet, FetchError> {
|
||||
};
|
||||
}
|
||||
let text = response.text().await?;
|
||||
// Classify before parsing the tweet (see [`parse_syndication_body`]).
|
||||
parse_syndication_body(&text)?;
|
||||
Tweet::from_syndication_json(&text).map_err(FetchError::Json)
|
||||
// Classify before building the tweet (see [`parse_syndication_body`]), and
|
||||
// build it from the value that classification already parsed: this used to
|
||||
// scan and allocate the whole body twice.
|
||||
let body = parse_syndication_body(&text)?;
|
||||
Tweet::from_syndication_value(body).map_err(FetchError::Json)
|
||||
}
|
||||
|
||||
/// Parses and classifies a syndication response body. `Ok` means the body is
|
||||
/// a real tweet payload; `Err` carries the permanent error class:
|
||||
/// Parses and classifies a syndication response body. `Ok` carries the parsed
|
||||
/// body on for the caller to build the tweet from — the same value, so the
|
||||
/// text is never parsed twice; `Err` carries the permanent error class:
|
||||
/// - `NotFound`: an `errors` array (deleted/blocked) or a `TweetTombstone`
|
||||
/// **with a reason** — "This Post was deleted by the Post author." /
|
||||
/// "This Post is from a suspended account." (the tweet is gone).
|
||||
@@ -227,8 +230,13 @@ impl Tweet {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_syndication_json(raw_json: &str) -> Result<Self, serde_json::Error> {
|
||||
let json: model::SyndicationTweet = serde_json::from_str(raw_json)?;
|
||||
/// Builds a tweet from an already-parsed syndication body. Takes the value
|
||||
/// rather than JSON text so a caller that had to parse it anyway (the
|
||||
/// fetch path classifies the raw shape; the auth fallback builds the shape
|
||||
/// itself) does not pay for a second scan — `from_value` moves the strings
|
||||
/// out instead.
|
||||
pub fn from_syndication_value(body: serde_json::Value) -> Result<Self, serde_json::Error> {
|
||||
let json: model::SyndicationTweet = serde_json::from_value(body)?;
|
||||
let id = json.id_str;
|
||||
// Expand the user's t.co short links to their real destinations and
|
||||
// strip the appended media short link, mirroring FxEmbed's linkFixer
|
||||
@@ -430,7 +438,7 @@ mod tests {
|
||||
"entities": { "urls": [] },
|
||||
"mediaDetails": []
|
||||
});
|
||||
let tweet = Tweet::from_syndication_json(&raw.to_string()).unwrap();
|
||||
let tweet = Tweet::from_syndication_value(raw).unwrap();
|
||||
// The appended media short link is stripped, then entities decoded.
|
||||
assert_eq!(tweet.text, ">^ω^< & more 'quoted'");
|
||||
assert_eq!(tweet.author, "O'Brien");
|
||||
@@ -489,7 +497,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
]));
|
||||
let tweet = Tweet::from_syndication_json(&raw.to_string()).unwrap();
|
||||
let tweet = Tweet::from_syndication_value(raw).unwrap();
|
||||
let fetched: Fetched = tweet.into();
|
||||
assert_eq!(
|
||||
fetched.source_url,
|
||||
@@ -535,7 +543,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
]));
|
||||
let tweet = Tweet::from_syndication_json(&raw.to_string()).unwrap();
|
||||
let tweet = Tweet::from_syndication_value(raw).unwrap();
|
||||
assert!(matches!(&tweet.media[0], Media::Animated { .. }));
|
||||
}
|
||||
|
||||
@@ -562,7 +570,7 @@ mod tests {
|
||||
"user": { "name": "N", "screen_name": "h" },
|
||||
"mediaDetails": []
|
||||
});
|
||||
let tweet = Tweet::from_syndication_json(&raw.to_string()).unwrap();
|
||||
let tweet = Tweet::from_syndication_value(raw).unwrap();
|
||||
assert_eq!(tweet.text, visible, "left a partial link in {text:?}");
|
||||
assert!(!tweet.caption().contains("t.co"), "{text:?}");
|
||||
}
|
||||
@@ -587,7 +595,7 @@ mod tests {
|
||||
},
|
||||
"mediaDetails": []
|
||||
});
|
||||
let tweet = Tweet::from_syndication_json(&raw.to_string()).unwrap();
|
||||
let tweet = Tweet::from_syndication_value(raw).unwrap();
|
||||
assert_eq!(
|
||||
tweet.text,
|
||||
"Test Tweet with @mentionThis $twtr http://bit.ly/2pUk4be #hashtag"
|
||||
@@ -606,7 +614,7 @@ mod tests {
|
||||
"user": { "name": "N", "screen_name": "h" },
|
||||
"mediaDetails": []
|
||||
});
|
||||
let tweet = Tweet::from_syndication_json(&raw.to_string()).unwrap();
|
||||
let tweet = Tweet::from_syndication_value(raw).unwrap();
|
||||
assert_eq!(tweet.text, "check #tag");
|
||||
}
|
||||
|
||||
@@ -629,7 +637,7 @@ mod tests {
|
||||
},
|
||||
"mediaDetails": []
|
||||
});
|
||||
let tweet = Tweet::from_syndication_json(&raw.to_string()).unwrap();
|
||||
let tweet = Tweet::from_syndication_value(raw).unwrap();
|
||||
assert_eq!(tweet.text, "see for context");
|
||||
assert!(!tweet.caption().contains("t.co"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user