mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-09-23 23:32:05 +00:00
fix(commands): make /set_format and /clear_cache actually parse
teloxide's `split` parser takes exactly one space-separated token per
field, so `/set_format <site> <format>` — a two-token command — never
parsed: `Command::parse` failed, `message_handler` fell through to the URL
flow, and the user got silence. `/clear_cache` without its optional link
failed the same way ("too few arguments"), so clearing everything was
unreachable. Both now use the crate's `parse_arg_remainder` (whole
remainder, trimmed), which is what their executors were already written
against (`split_once(char::is_whitespace)`).
Found by driving the real binary against a scripted fake Bot API: the
`/set_format` replies never appeared while `/set_template` and the other
single-token commands did. `every_documented_invocation_parses` now pins
every documented form, which is what should have caught it.
The placeholder validation and `-` reset added earlier only work now that
the command reaches its executor at all.
This commit is contained in:
@@ -24,7 +24,7 @@ Debug command: `/debug <url>` runs the same `x_media::site::fetch` and replies w
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
The `/test <url>` command runs the ordinary link pipeline (`urls::url_media`) with `PostSend::Suppressed`: the media is sent and cached like any other link, but the chat's `forward_channel_id`/`edit_before_forward` are ignored, so a test never forwards to the channel and never opens the edit prompt (retries and dead-letter notifications behave as usual). Both commands use a custom `parse_arg_remainder` parser (whole remainder, trimmed) because teloxide's built-in `split` parser takes exactly one space-separated token.
|
The `/test <url>` command runs the ordinary link pipeline (`urls::url_media`) with `PostSend::Suppressed`: the media is sent and cached like any other link, but the chat's `forward_channel_id`/`edit_before_forward` are ignored, so a test never forwards to the channel and never opens the edit prompt (retries and dead-letter notifications behave as usual). `/test`, `/debug`, `/set_format` and `/clear_cache` use the custom `parse_arg_remainder` parser (whole remainder, trimmed) because teloxide's built-in `split` parser takes exactly one space-separated token per field: `/set_format <site> <format>` never parsed with it (and `/clear_cache` without an argument did not either), and a command that fails to parse falls through to the URL flow in silence. `commands::tests::every_documented_invocation_parses` pins every documented form against exactly that.
|
||||||
|
|
||||||
The inline path (`handlers/inline.rs`) hands media URLs straight to Telegram, which fetches them itself and cannot send site-specific headers — so `x_media::site::needs_media_headers(url)` (true exactly where a site's `media_headers` is non-empty, i.e. pixiv's pximg.net) marks the media that must be skipped instead of shipped broken; locally produced media (ugoira MP4, bsky remux) fails `Url::parse` and is skipped the same way. Inline results are therefore URL-only by construction.
|
The inline path (`handlers/inline.rs`) hands media URLs straight to Telegram, which fetches them itself and cannot send site-specific headers — so `x_media::site::needs_media_headers(url)` (true exactly where a site's `media_headers` is non-empty, i.e. pixiv's pximg.net) marks the media that must be skipped instead of shipped broken; locally produced media (ugoira MP4, bsky remux) fails `Url::parse` and is skipped the same way. Inline results are therefore URL-only by construction.
|
||||||
|
|
||||||
|
|||||||
@@ -42,12 +42,12 @@ pub(crate) enum Command {
|
|||||||
BotDict,
|
BotDict,
|
||||||
#[command(
|
#[command(
|
||||||
description = "Set site caption format (- to reset)",
|
description = "Set site caption format (- to reset)",
|
||||||
parse_with = "split"
|
parse_with = parse_arg_remainder
|
||||||
)]
|
)]
|
||||||
SetFormat(String),
|
SetFormat(String),
|
||||||
#[command(
|
#[command(
|
||||||
description = "Clear link cache (admin; optional URL, else all)",
|
description = "Clear link cache (admin; optional URL, else all)",
|
||||||
parse_with = "split"
|
parse_with = parse_arg_remainder
|
||||||
)]
|
)]
|
||||||
ClearCache(String),
|
ClearCache(String),
|
||||||
#[command(
|
#[command(
|
||||||
@@ -987,6 +987,76 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_documented_invocation_parses() {
|
||||||
|
use teloxide::utils::command::BotCommands;
|
||||||
|
|
||||||
|
use super::Command;
|
||||||
|
|
||||||
|
// The README's forms, verbatim. teloxide's `split` parser accepts
|
||||||
|
// EXACTLY one token per `String` field, so a command documented with
|
||||||
|
// two arguments (or an optional one) silently stops parsing — and a
|
||||||
|
// command that does not parse falls through to the URL flow in
|
||||||
|
// silence.
|
||||||
|
type Check = fn(&Command) -> bool;
|
||||||
|
let cases: Vec<(&str, Check)> = vec![
|
||||||
|
("/start", |c| matches!(c, Command::Start)),
|
||||||
|
("/help", |c| matches!(c, Command::Help)),
|
||||||
|
("/settings", |c| matches!(c, Command::Settings)),
|
||||||
|
("/edit_before_forward", |c| {
|
||||||
|
matches!(c, Command::EditBeforeForward)
|
||||||
|
}),
|
||||||
|
("/remove_forward_channel", |c| {
|
||||||
|
matches!(c, Command::RemoveForwardChannel)
|
||||||
|
}),
|
||||||
|
("/bot_dict", |c| matches!(c, Command::BotDict)),
|
||||||
|
(
|
||||||
|
"/set_forward_channel @a_channel",
|
||||||
|
|c| matches!(c, Command::SetForwardChannel(a) if a == "@a_channel"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/set_template tpl",
|
||||||
|
|c| matches!(c, Command::SetTemplate(a) if a == "tpl"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/remove_template tpl",
|
||||||
|
|c| matches!(c, Command::RemoveTemplate(a) if a == "tpl"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/set_format twitter {author}: {title}",
|
||||||
|
|c| matches!(c, Command::SetFormat(a) if a == "twitter {author}: {title}"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/set_format twitter -",
|
||||||
|
|c| matches!(c, Command::SetFormat(a) if a == "twitter -"),
|
||||||
|
),
|
||||||
|
// Documented as "clear everything" when called without a link.
|
||||||
|
(
|
||||||
|
"/clear_cache",
|
||||||
|
|c| matches!(c, Command::ClearCache(a) if a.is_empty()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/clear_cache https://x.com/u/status/1",
|
||||||
|
|c| matches!(c, Command::ClearCache(a) if a == "https://x.com/u/status/1"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/test https://x.com/u/status/1",
|
||||||
|
|c| matches!(c, Command::Test(a) if a == "https://x.com/u/status/1"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/debug https://x.com/u/status/1",
|
||||||
|
|c| matches!(c, Command::Debug(a) if a == "https://x.com/u/status/1"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (text, ok) in cases {
|
||||||
|
match Command::parse(text, "") {
|
||||||
|
Ok(parsed) => assert!(ok(&parsed), "{text} parsed as the wrong variant"),
|
||||||
|
Err(e) => panic!("{text} did not parse: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_placeholder_finds_typos_only() {
|
fn unknown_placeholder_finds_typos_only() {
|
||||||
assert_eq!(unknown_placeholder("{author} — {title}"), None);
|
assert_eq!(unknown_placeholder("{author} — {title}"), None);
|
||||||
|
|||||||
Reference in New Issue
Block a user