mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-09-23 23:32:05 +00:00
feat(handlers): debounce inline queries to stop fetch storms while typing
Telegram fires an inline query on every keystroke and every prefix of a pasted URL (status/12, status/123, ...) matches the site patterns, so typing used to trigger a full 3-attempt fetch per keystroke. Answer only after the query has been stable for 800ms, dedupe repeats through Telegram's inline cache (explicit cache_time 300), and let a repeat of a query that produced no answer retry the fetch.
This commit is contained in:
@@ -764,16 +764,85 @@ pub async fn message_handler(bot: Bot, message: Message) -> Result<(), RequestEr
|
|||||||
respond(())
|
respond(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Debounce window for inline queries: Telegram fires an inline query on
|
||||||
|
/// every keystroke, and each prefix of a pasted URL (e.g. `.../status/12`,
|
||||||
|
/// `.../status/123`, ...) already matches the site patterns. Without a
|
||||||
|
/// debounce every keystroke triggers a fetch (3 attempts!) of a half-typed
|
||||||
|
/// post id. Only answer once the query has been stable for this long.
|
||||||
|
const INLINE_DEBOUNCE: std::time::Duration = std::time::Duration::from_millis(800);
|
||||||
|
|
||||||
|
/// Last seen inline query and whether it was already answered. Guards the
|
||||||
|
/// debounce timer: a repeat of an answered query is served by Telegram's
|
||||||
|
/// inline cache (see `cache_time`), not by another fetch.
|
||||||
|
struct InlineDebounceState {
|
||||||
|
query: String,
|
||||||
|
answered: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static INLINE_DEBOUNCE_STATE: LazyLock<parking_lot::Mutex<Option<InlineDebounceState>>> =
|
||||||
|
LazyLock::new(|| parking_lot::Mutex::new(None));
|
||||||
|
|
||||||
pub async fn inline_query_handler(bot: Bot, query: InlineQuery) -> Result<(), RequestError> {
|
pub async fn inline_query_handler(bot: Bot, query: InlineQuery) -> Result<(), RequestError> {
|
||||||
if query.query.is_empty() {
|
if query.query.is_empty() {
|
||||||
return respond(());
|
return respond(());
|
||||||
}
|
}
|
||||||
// Telegram fires an inline query on every keystroke; only run a fetch
|
// Only run a fetch for something that is actually a supported post URL.
|
||||||
// (3 attempts!) for something that is actually a supported post URL, so
|
|
||||||
// typing does not hammer the source sites.
|
|
||||||
if x_media::site::cache_key(&query.query).is_none() {
|
if x_media::site::cache_key(&query.query).is_none() {
|
||||||
return respond(());
|
return respond(());
|
||||||
}
|
}
|
||||||
|
// Debounce: record the query and answer only after it has been stable for
|
||||||
|
// INLINE_DEBOUNCE (the timer below). An already-answered repeat of the
|
||||||
|
// same query is left to Telegram's inline cache instead of re-fetching.
|
||||||
|
{
|
||||||
|
let mut state = INLINE_DEBOUNCE_STATE.lock();
|
||||||
|
if let Some(prev) = state.as_ref()
|
||||||
|
&& prev.query == query.query
|
||||||
|
&& prev.answered
|
||||||
|
{
|
||||||
|
return respond(());
|
||||||
|
}
|
||||||
|
*state = Some(InlineDebounceState {
|
||||||
|
query: query.query.clone(),
|
||||||
|
answered: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let query_text = query.query.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(INLINE_DEBOUNCE).await;
|
||||||
|
// Only the last query of a typing burst survives: earlier timers see
|
||||||
|
// the query changed and give up without answering.
|
||||||
|
{
|
||||||
|
let mut state = INLINE_DEBOUNCE_STATE.lock();
|
||||||
|
let Some(state) = state.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if state.query != query_text || state.answered {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Claim the answer so a repeat of the same query cannot start a
|
||||||
|
// second fetch; reset below when no answer was produced.
|
||||||
|
state.answered = true;
|
||||||
|
}
|
||||||
|
match answer_inline_query(bot, query).await {
|
||||||
|
Ok(true) => {}
|
||||||
|
// No results produced (or nothing to answer): let a repeat of the
|
||||||
|
// same query retry the fetch.
|
||||||
|
Ok(false) | Err(_) => {
|
||||||
|
let mut state = INLINE_DEBOUNCE_STATE.lock();
|
||||||
|
if let Some(state) = state.as_mut()
|
||||||
|
&& state.query == query_text
|
||||||
|
{
|
||||||
|
state.answered = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
respond(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the post behind an inline query and answers it. The caller has
|
||||||
|
/// already applied the debounce. Returns `true` when an answer was sent.
|
||||||
|
async fn answer_inline_query(bot: Bot, query: InlineQuery) -> Result<bool, RequestError> {
|
||||||
log::info!("inline query: {}", query.query);
|
log::info!("inline query: {}", query.query);
|
||||||
match x_media::site::fetch(&query.query).await {
|
match x_media::site::fetch(&query.query).await {
|
||||||
Ok(Some(fetched)) => {
|
Ok(Some(fetched)) => {
|
||||||
@@ -822,13 +891,18 @@ pub async fn inline_query_handler(bot: Bot, query: InlineQuery) -> Result<(), Re
|
|||||||
results.push(result);
|
results.push(result);
|
||||||
}
|
}
|
||||||
if !results.is_empty() {
|
if !results.is_empty() {
|
||||||
bot.answer_inline_query(query.id, results).await?;
|
// Explicit cache window: repeats of the same query within 5
|
||||||
|
// minutes are served by Telegram without hitting the bot.
|
||||||
|
bot.answer_inline_query(query.id, results)
|
||||||
|
.cache_time(300)
|
||||||
|
.await?;
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(e) => log::error!("inline fetch {}: {e}", query.query),
|
Err(e) => log::error!("inline fetch {}: {e}", query.query),
|
||||||
}
|
}
|
||||||
respond(())
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn callback_query_handler(bot: Bot, query: CallbackQuery) -> Result<(), RequestError> {
|
pub async fn callback_query_handler(bot: Bot, query: CallbackQuery) -> Result<(), RequestError> {
|
||||||
|
|||||||
Reference in New Issue
Block a user