mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-09-23 23:32:05 +00:00
fix(twitter): decode API HTML entities so captions escape exactly once
Twitter's syndication and GraphQL APIs return tweet text and display names pre-escaped for HTML (> < & '); the caption builder escaped the text again, so sent messages showed literal entities (e.g. >^ω^< came back as >^ω^<). from_syndication_json now decodes the API text before storing it — both the syndication path and the TWITTER_AUTH_TOKEN GraphQL fallback route through it — so the caption escapes exactly once and renders correctly. The /test report is a plain-text message but printed the pre-escaped caption and render fields; it now HTML-decodes them for display so the report shows the rendered text.
This commit is contained in:
@@ -415,14 +415,20 @@ fn test_parse_report(
|
||||
lines.push(format!("source_url: {source_url}"));
|
||||
lines.push(format!("title: {title}"));
|
||||
if let Some((author, author_url, _title, tags)) = render {
|
||||
lines.push(format!("author: {author}"));
|
||||
// The render fields are pre-escaped for HTML captions; decode them
|
||||
// so the plain-text report shows the text as it will be rendered
|
||||
// (no visible & / < / >).
|
||||
lines.push(format!(
|
||||
"author: {}",
|
||||
html_escape::decode_html_entities(author)
|
||||
));
|
||||
lines.push(format!("author_url: {author_url}"));
|
||||
lines.push(format!("tags: {tags}"));
|
||||
lines.push(format!("tags: {}", html_escape::decode_html_entities(tags)));
|
||||
}
|
||||
lines.push(format!("sensitive: {sensitive}"));
|
||||
lines.push(format!(
|
||||
"caption: {}",
|
||||
x_media::site::truncate_caption(caption)
|
||||
x_media::site::truncate_caption(&html_escape::decode_html_entities(caption))
|
||||
));
|
||||
lines.push(format!("media ({}):", media.len()));
|
||||
for (i, item) in media.iter().enumerate() {
|
||||
@@ -500,6 +506,38 @@ mod tests {
|
||||
assert!(report.contains("media (0):"), "{report}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_report_decodes_html_entities_for_display() {
|
||||
// The report is a plain-text message: pre-escaped caption fields and
|
||||
// the HTML caption must be shown decoded (as rendered), never with
|
||||
// visible & / < / >.
|
||||
let report = test_parse_report(
|
||||
"https://x.com/u/status/1",
|
||||
"twitter",
|
||||
"https://x.com/u/status/1",
|
||||
"A & B <C>",
|
||||
Some((
|
||||
"A & B",
|
||||
"https://x.com/u",
|
||||
"A & B <C>",
|
||||
"#a & #b",
|
||||
)),
|
||||
false,
|
||||
"<a href=\"https://x.com/u\">A & B</a>: C <D> & E",
|
||||
&[],
|
||||
);
|
||||
assert!(report.contains("title: A & B <C>"), "{report}");
|
||||
assert!(report.contains("author: A & B"), "{report}");
|
||||
assert!(report.contains("tags: #a & #b"), "{report}");
|
||||
assert!(
|
||||
report.contains("caption: <a href=\"https://x.com/u\">A & B</a>: C <D> & E"),
|
||||
"{report}"
|
||||
);
|
||||
for entity in ["&", "<", ">"] {
|
||||
assert!(!report.contains(entity), "unexpected {entity} in: {report}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_report_is_capped() {
|
||||
// 200 media lines ≈ 8 KB, comfortably over the cap.
|
||||
|
||||
Reference in New Issue
Block a user