mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-09-23 23:32:05 +00:00
fix(commands): make /debug preview the caption a link would actually send
`/debug` reported `Fetched::caption`, the site's built-in caption, so a chat's `/set_format` override never showed up in the preview — the command looked like a no-op, and the `/set_format` success reply now tells users to preview with `/debug`, which made that advice wrong. `preview_caption` mirrors the send paths instead: the per-site format override (`caption_from_fields`, empty → built-in) plus the long-post quoting. Reported from a live run against the test bot.
This commit is contained in:
@@ -20,7 +20,7 @@ Telegram update → Dispatcher (polling or axum webhook) → dptree branches
|
||||
|
||||
Message flow: `message_handler` extracts URLs (from `url`/`text_link` entities, text + caption, deduped) → `x_media::site::fetch(url)` → `Fetched` → builds a `Task` → `send::send_media_sequence` (media groups ≤ 10, caption on first item) or `send::send_animation`. On Telegram URL-fetch failure or size error (`send_batch_via_upload`): download via `x_media::site::download_media` to a temp file (≤ 10 MiB), sniff magic bytes (`sniff_ext`), upload via multipart; oversized items fall back to `fallback_url`. On failure: `enqueue_retry` persists resume-state `Task` into the SQLite queue → workers lease (120 s lock TTL) → retry with exponential backoff (≤ 30 s, `MAX_RETRIES = 2`) → dead-letter → `notify_failure`. Success → `post_send_actions`: edit-before-forward prompt with inline buttons, or `copy_messages` to the bound forward channel.
|
||||
|
||||
Debug command: `/debug <url>` runs the same `x_media::site::fetch` and replies with `debug_report` (`handlers/commands.rs`) — site id, normalized cache key, source URL, title/author/tags, sensitive flag, caption and the media list — nothing is sent, cached or forwarded; the report is capped at 4000 chars and sent with HTML parse mode: raw fields are escaped, and the caption is wrapped in a `<blockquote>` so it renders exactly like the sent media caption (escaped text and links included).
|
||||
Debug command: `/debug <url>` runs the same `x_media::site::fetch` and replies with `debug_report` (`handlers/commands.rs`) — site id, normalized cache key, source URL, title/author/tags, sensitive flag, caption and the media list — nothing is sent, cached or forwarded; the report is capped at 4000 chars and sent with HTML parse mode: raw fields are escaped, and the caption is wrapped in a `<blockquote>` so it renders exactly like the sent media caption (escaped text and links included). The caption it shows is `preview_caption`'s: the chat's per-site format override plus the long-post quoting, i.e. exactly what the send paths produce — showing the raw built-in caption made `/set_format` look like a no-op, and the `/set_format` success reply points users at `/debug` to preview.
|
||||
|
||||
User-facing failure text is a function of the error class, never one generic sentence: `urls::fetch_error_message` maps `FetchError::NotFound` (post gone), `Sensitive` (withheld, needs `TWITTER_AUTH_TOKEN`), `Blocked` (source risk control), `Disabled { site }` (a registered site switched off — pixiv without a token, the one case `fetch` answers `Err` instead of `Ok(None)`) and `Transient`/`Http` (source down) apart. The same distinction drives the group hint: a supported link posted in a group (not a channel) gets one `GROUP_LINK_HINT` reply, because the link pipeline is private-chat only.
|
||||
|
||||
|
||||
@@ -619,6 +619,24 @@ pub(crate) async fn execute_command(
|
||||
.await?;
|
||||
}
|
||||
Ok(Some(fetched)) => {
|
||||
// The preview must show what a link would actually send:
|
||||
// the chat's per-site format override plus the long-post
|
||||
// quoting. Rendering the raw built-in caption here made
|
||||
// `/set_format` look like it did nothing.
|
||||
let format = CHAT_STORE
|
||||
.get(message.chat.id.0)
|
||||
.await
|
||||
.message_format
|
||||
.get(fetched.site_name())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let caption = preview_caption(
|
||||
&format,
|
||||
&fetched.caption,
|
||||
&fetched.source_url,
|
||||
fetched.render_fields(),
|
||||
CONFIG.caption_quote_text_chars,
|
||||
);
|
||||
let report = debug_report(
|
||||
url,
|
||||
fetched.site_name(),
|
||||
@@ -627,7 +645,7 @@ pub(crate) async fn execute_command(
|
||||
&fetched.content,
|
||||
fetched.render_fields(),
|
||||
fetched.sensitive,
|
||||
&fetched.caption,
|
||||
&caption,
|
||||
&fetched.media,
|
||||
);
|
||||
// HTML report: the caption renders inside a <blockquote>
|
||||
@@ -682,6 +700,31 @@ const MAX_DEBUG_REPORT_CHARS: usize = 4000;
|
||||
/// message, so it must stay under Telegram's 4096-char limit.
|
||||
const MAX_DEBUG_DUMP_CHARS: usize = 3500;
|
||||
|
||||
/// The caption a link would actually send for this chat: the per-site format
|
||||
/// override (empty = the site's built-in caption) and, on a long post, the
|
||||
/// same text quoting the send paths apply. `/debug` shows this so the preview
|
||||
/// cannot drift from what the send paths produce.
|
||||
fn preview_caption(
|
||||
format: &str,
|
||||
built_in: &str,
|
||||
url: &str,
|
||||
fields: Option<(&str, &str, &str, &str, &str)>,
|
||||
quote_chars: usize,
|
||||
) -> String {
|
||||
let caption = match fields {
|
||||
// Same call the send paths make through `Fetched::caption_with`: an
|
||||
// empty format falls back to the built-in caption.
|
||||
Some((author, author_url, title, content, tags)) => x_media::site::caption_from_fields(
|
||||
format, built_in, url, author, author_url, title, content, tags,
|
||||
),
|
||||
None => x_media::site::truncate_caption(built_in),
|
||||
};
|
||||
let text = fields
|
||||
.map(|(_, _, title, content, _)| x_media::site::compose_text(title, content))
|
||||
.unwrap_or_default();
|
||||
crate::send::quote_long_caption(&caption, &text, quote_chars).into_owned()
|
||||
}
|
||||
|
||||
/// Builds the HTML report for the `/debug` command: what the parser produced
|
||||
/// for a link (site, canonical URL, title/author/tags, caption and the media
|
||||
/// list) — no media is sent and nothing is cached or forwarded. Sent with
|
||||
@@ -764,7 +807,9 @@ fn debug_report(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MAX_DEBUG_REPORT_CHARS, debug_report, settings_text, unknown_placeholder};
|
||||
use super::{
|
||||
MAX_DEBUG_REPORT_CHARS, debug_report, preview_caption, settings_text, unknown_placeholder,
|
||||
};
|
||||
use x_media::media::Media;
|
||||
|
||||
#[test]
|
||||
@@ -1057,6 +1102,71 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_caption_applies_the_chat_format_and_the_long_post_quote() {
|
||||
let fields = Some((
|
||||
"Author",
|
||||
"https://x.com/u",
|
||||
"Pinned title",
|
||||
"Pinned body",
|
||||
"#tag",
|
||||
));
|
||||
|
||||
// No format override → the site's built-in caption, untouched.
|
||||
assert_eq!(
|
||||
preview_caption(
|
||||
"",
|
||||
"built-in caption",
|
||||
"https://x.com/u/status/1",
|
||||
fields,
|
||||
200
|
||||
),
|
||||
"built-in caption"
|
||||
);
|
||||
|
||||
// The bug this pins: `/debug` used to print the built-in caption even
|
||||
// with a format set, so `/set_format` looked like it did nothing.
|
||||
let formatted = preview_caption(
|
||||
"{author} · {title}",
|
||||
"built-in caption",
|
||||
"https://x.com/u/status/1",
|
||||
fields,
|
||||
200,
|
||||
);
|
||||
assert_eq!(formatted, "Author · Pinned title");
|
||||
|
||||
// `{url}` comes from the canonical post URL, as in the send paths.
|
||||
assert_eq!(
|
||||
preview_caption(
|
||||
"{url} {title}",
|
||||
"built-in",
|
||||
"https://x.com/u/status/1",
|
||||
fields,
|
||||
200
|
||||
),
|
||||
"https://x.com/u/status/1 Pinned title"
|
||||
);
|
||||
|
||||
// A long post's text is quoted exactly like the send paths quote it.
|
||||
let long = "正".repeat(300);
|
||||
let fields = Some(("Author", "https://x.com/u", "", long.as_str(), ""));
|
||||
let quoted = preview_caption(
|
||||
"",
|
||||
"https://x.com/u/status/1\n<a href=\"https://x.com/u\">Author</a>: 正…",
|
||||
"https://x.com/u/status/1",
|
||||
fields,
|
||||
200,
|
||||
);
|
||||
assert!(quoted.contains("<blockquote expandable>"), "{quoted}");
|
||||
|
||||
// Without render fields (a site that does not expose them) the
|
||||
// built-in caption is all there is.
|
||||
assert_eq!(
|
||||
preview_caption("", "built-in", "https://x.com/u/status/1", None, 200),
|
||||
"built-in"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_placeholder_finds_typos_only() {
|
||||
assert_eq!(unknown_placeholder("{author} — {title}"), None);
|
||||
|
||||
Reference in New Issue
Block a user