use std::sync::atomic::{AtomicU8, Ordering};
use teloxide::Bot;
use teloxide::types::{ChatId, ThreadId};
pub(crate) fn receiver_for(is_dm: bool, user_id: i64) -> Option<i64> {
if is_dm { None } else { Some(user_id) }
}
pub(crate) fn build_body(
chat_id: i64,
thread_id: Option<ThreadId>,
receiver_user_id: i64,
text: &str,
parse_html: bool,
) -> serde_json::Value {
let mut body = serde_json::json!({
"chat_id": chat_id,
"text": text,
"receiver_user_id": receiver_user_id,
});
if parse_html {
body["parse_mode"] = serde_json::json!("HTML");
}
if let Some(t) = thread_id {
body["message_thread_id"] = serde_json::json!(t.0.0);
}
body
}
pub(crate) async fn send_one(
token: &str,
chat_id: i64,
thread_id: Option<ThreadId>,
receiver_user_id: i64,
text: &str,
parse_html: bool,
) -> bool {
let body = build_body(chat_id, thread_id, receiver_user_id, text, parse_html);
post(token, "sendMessage", &body).await == Outcome::Sent
}
pub(crate) fn build_rich_body(
chat_id: i64,
thread_id: Option<ThreadId>,
receiver_user_id: i64,
markdown: &str,
) -> serde_json::Value {
let mut body = super::rich::api::build_body(chat_id, thread_id, markdown);
body["receiver_user_id"] = serde_json::json!(receiver_user_id);
body
}
static RICH_SCOPING: AtomicU8 = AtomicU8::new(RICH_UNKNOWN);
const RICH_UNKNOWN: u8 = 0;
const RICH_SUPPORTED: u8 = 1;
const RICH_UNSUPPORTED: u8 = 2;
pub(crate) async fn try_send_rich(
token: &str,
chat_id: i64,
thread_id: Option<ThreadId>,
receiver_user_id: i64,
markdown: &str,
) -> bool {
if RICH_SCOPING.load(Ordering::Relaxed) == RICH_UNSUPPORTED {
return false;
}
let body = build_rich_body(chat_id, thread_id, receiver_user_id, markdown);
match post(token, "sendRichMessage", &body).await {
Outcome::Sent => {
if RICH_SCOPING.swap(RICH_SUPPORTED, Ordering::Relaxed) == RICH_UNKNOWN {
tracing::info!(
"Telegram: sendRichMessage accepts receiver_user_id, \
scoped command replies keep native rich rendering"
);
}
true
}
Outcome::Rejected => {
RICH_SCOPING.store(RICH_UNSUPPORTED, Ordering::Relaxed);
tracing::info!(
"Telegram: sendRichMessage does not accept receiver_user_id, \
scoped command replies will render as HTML from here on"
);
false
}
Outcome::Transport => false,
}
}
pub(crate) async fn send_html_chunks(
token: &str,
chat_id: i64,
thread_id: Option<ThreadId>,
receiver_user_id: i64,
chunks: &[&str],
) -> usize {
let mut delivered = 0usize;
for chunk in chunks {
if !send_one(token, chat_id, thread_id, receiver_user_id, chunk, true).await {
break;
}
delivered += 1;
}
delivered
}
pub(crate) async fn send_ack(
bot: &Bot,
chat_id: ChatId,
thread_id: Option<ThreadId>,
receiver: Option<i64>,
text: &str,
) -> std::result::Result<(), teloxide::RequestError> {
if let Some(rx) = receiver
&& send_one(bot.token(), chat_id.0, thread_id, rx, text, false).await
{
return Ok(());
}
super::intermediates::send_retrying_rate_limit("command reply", || {
super::send::message_in_thread(bot, chat_id, thread_id, text)
})
.await
.map(|_| ())
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Outcome {
Sent,
Rejected,
Transport,
}
async fn post(token: &str, method: &str, body: &serde_json::Value) -> Outcome {
let url = format!("https://api.telegram.org/bot{token}/{method}");
let resp = match reqwest::Client::new().post(&url).json(body).send().await {
Ok(r) => r,
Err(e) => {
tracing::warn!("Telegram: ephemeral {method} transport error: {e}, sending publicly");
return Outcome::Transport;
}
};
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
let parsed: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();
if status.is_success() && parsed.get("ok").and_then(serde_json::Value::as_bool) == Some(true) {
return Outcome::Sent;
}
let desc = parsed
.get("description")
.and_then(serde_json::Value::as_str)
.unwrap_or(&text);
tracing::warn!("Telegram: ephemeral {method} rejected ({status}): {desc}, sending publicly");
Outcome::Rejected
}