use std::future::Future;
use std::time::Duration;
use teloxide::Bot;
use teloxide::payloads::{EditMessageTextSetters, SendMessageSetters};
use teloxide::prelude::Requester;
use teloxide::types::{ChatId, InlineKeyboardMarkup, MessageId, ParseMode};
pub const MAX_DEFERRED_WAIT: Duration = Duration::from_secs(35);
const RICH_429_FALLBACK_WAIT_SECS: u64 = 30;
pub enum EditErr {
RetryAfter(Duration),
Fatal(String),
}
pub fn classify(e: &teloxide::RequestError) -> EditErr {
match e {
teloxide::RequestError::RetryAfter(secs) => EditErr::RetryAfter(secs.duration()),
other => EditErr::Fatal(other.to_string()),
}
}
pub fn classify_str(e: &str) -> EditErr {
if let Some(rest) = e
.strip_prefix("Retry after ")
.and_then(|r| r.strip_suffix('s'))
&& let Ok(secs) = rest.parse::<u64>()
{
return EditErr::RetryAfter(Duration::from_secs(secs));
}
if e.contains("(429)") {
return EditErr::RetryAfter(Duration::from_secs(RICH_429_FALLBACK_WAIT_SECS));
}
EditErr::Fatal(e.to_string())
}
pub fn spawn_deferred<E, F, Fut, G, Fut2>(wait: Duration, retry: F, exhausted: G)
where
E: std::fmt::Display + Send + 'static,
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Result<(), E>> + Send + 'static,
G: FnOnce() -> Fut2 + Send + 'static,
Fut2: Future<Output = ()> + Send + 'static,
{
let wait = wait.min(MAX_DEFERRED_WAIT);
tokio::spawn(async move {
tokio::time::sleep(wait).await;
match retry().await {
Ok(()) => {
tracing::info!("Telegram: deferred UI-edit retry landed after {wait:?} wait");
}
Err(e) => {
tracing::warn!(
"Telegram: deferred UI-edit retry exhausted after {wait:?} wait ({e}) — running fallback"
);
exhausted().await;
}
}
});
}
pub fn edit_text_ui(
bot: Bot,
chat_id: ChatId,
message_id: MessageId,
text: String,
parse_html: bool,
markup: Option<InlineKeyboardMarkup>,
label: &'static str,
) {
let bot_for_fire = bot.clone();
let text_for_fire = text.clone();
let markup_for_fire = markup.clone();
let fire = move || {
let bot = bot_for_fire.clone();
let text = text_for_fire.clone();
let markup = markup_for_fire.clone();
async move {
let mut req = bot.edit_message_text(chat_id, message_id, &text);
if parse_html {
req = req.parse_mode(ParseMode::Html);
}
if let Some(kb) = markup {
req = req.reply_markup(kb);
}
req.await.map(|_| ())
}
};
tokio::spawn(async move {
let _ = super::governor::edit_admission(
&bot,
chat_id,
message_id,
super::governor::EditClass::Interactive,
String::new(),
false,
)
.await;
match fire().await {
Ok(()) => {}
Err(e) => match classify(&e) {
EditErr::RetryAfter(wait) => {
let bot_fb = bot.clone();
let text_fb = text.clone();
let markup_fb = markup.clone();
spawn_deferred(wait, fire, move || async move {
tracing::warn!(
"Telegram: {label} deferred retry exhausted (still rate-limited) — falling back to fresh send"
);
let mut req = bot_fb.send_message(chat_id, &text_fb);
if parse_html {
req = req.parse_mode(ParseMode::Html);
}
if let Some(kb) = markup_fb {
req = req.reply_markup(kb);
}
if let Err(e) = req.await {
tracing::warn!("Telegram: {label} fallback send failed: {e}");
}
});
}
EditErr::Fatal(msg) => {
tracing::warn!("Telegram: {label} failed: {msg}");
}
},
}
});
}