From d1f2ae09b2d5af41a6d816bd999b321986c69348 Mon Sep 17 00:00:00 2001 From: YoursFunny Date: Thu, 24 Sep 2026 15:28:59 +0800 Subject: [PATCH] fix(rate-limit): charge plain messages and chat actions to the global bucket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit send_media_group, send_animation and copy_messages paced themselves against both buckets; send_message and send_chat_action paced against neither. A dead-letter storm — or one error reply per failed item, plus the action refresh loop's repeated typing requests — could therefore burst past Telegram's per-bot30/s ceiling with only429s left to absorb it, exactly what the bot-wide bucket exists to prevent. Both now acquire one global token like their siblings (per-chat pacing for messages already happens at their call sites; chat actions are cheap and frequent, so only the global bucket applies to them). --- crates/xmedia-bot/src/media_sender/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/xmedia-bot/src/media_sender/mod.rs b/crates/xmedia-bot/src/media_sender/mod.rs index 8d9a286..1c36397 100644 --- a/crates/xmedia-bot/src/media_sender/mod.rs +++ b/crates/xmedia-bot/src/media_sender/mod.rs @@ -185,6 +185,10 @@ impl MediaSender for Bot { reply_markup: Option, ) -> BoxFuture<'_, Result> { Box::pin(async move { + // Plain messages never met a bucket: a dead-letter storm (or one + // error reply per failed item) could burst past Telegram's + // per-bot ceiling with only 429s left to absorb it. + crate::rate_limit::acquire_global(1.0).await; let mut request = ::send_message(self, chat_id, text); if let Some(reply_to) = reply_to { request = request @@ -272,6 +276,10 @@ impl MediaSender for Bot { action: ChatAction, ) -> BoxFuture<'_, Result<(), RequestError>> { Box::pin(async move { + // Same gap as send_message: actions count against the bot-wide + // budget too (see there); the refresh loop behind + // `run_with_chat_action` makes them frequent enough to matter. + crate::rate_limit::acquire_global(1.0).await; // teloxide's `send_chat_action` returns `Result` (its // unit marker type); map the success to `()`. ::send_chat_action(self, chat_id, action)