use std::collections::VecDeque;
use std::sync::Mutex;
use serde_json::Value;
use teloxide::stop::{StopFlag, StopToken, mk_stop_token};
use teloxide::types::Update;
use teloxide::update_listeners::StatefulListener;
static RAW_STASH: Mutex<VecDeque<((i64, i32), Value)>> = Mutex::new(VecDeque::new());
const STASH_CAP: usize = 128;
pub(crate) fn stash_raw_message(chat_id: i64, message_id: i32, raw: Value) {
let mut q = RAW_STASH.lock().unwrap_or_else(|e| e.into_inner());
q.retain(|((c, m), _)| !(*c == chat_id && *m == message_id));
q.push_back(((chat_id, message_id), raw));
while q.len() > STASH_CAP {
q.pop_front();
}
}
pub(crate) fn take_raw_message(chat_id: i64, message_id: i32) -> Option<Value> {
let mut q = RAW_STASH.lock().unwrap_or_else(|e| e.into_inner());
let idx = q
.iter()
.position(|((c, m), _)| *c == chat_id && *m == message_id)?;
q.remove(idx).map(|(_, v)| v)
}
pub(crate) fn raw_forward_origin(raw: &Value) -> Option<String> {
let origin = raw.get("forward_origin")?;
if let Some(u) = origin.get("sender_user") {
let mut label = u
.get("first_name")
.and_then(|v| v.as_str())
.unwrap_or("someone")
.to_string();
if let Some(last) = u.get("last_name").and_then(|v| v.as_str()) {
label.push(' ');
label.push_str(last);
}
if u.get("is_bot").and_then(|v| v.as_bool()).unwrap_or(false) {
label.push_str(" (bot)");
}
return Some(label);
}
if let Some(name) = origin.get("sender_user_name").and_then(|v| v.as_str()) {
return Some(name.to_string());
}
for key in ["sender_chat", "chat"] {
if let Some(title) = origin
.get(key)
.and_then(|c| c.get("title"))
.and_then(|v| v.as_str())
{
return Some(title.to_string());
}
}
Some("an unknown origin".to_string())
}
pub(crate) fn raw_content_for_agent(raw: &Value) -> String {
const DROP_KEYS: &[&str] = &[
"from",
"chat",
"date",
"message_id",
"edit_date",
"forward_origin",
"has_protected_content",
"sender_chat",
"via_bot",
"message_thread_id",
];
let mut content = raw.clone();
if let Some(obj) = content.as_object_mut() {
for k in DROP_KEYS {
obj.remove(*k);
}
}
let pretty = serde_json::to_string_pretty(&content).unwrap_or_else(|_| content.to_string());
crate::utils::truncate_str(&pretty, 4096).to_string()
}
struct RawPollState {
http: reqwest::Client,
token: String,
offset: i64,
pending: VecDeque<Update>,
flag: StopFlag,
stop_token: StopToken,
}
async fn poll_once(st: &mut RawPollState) {
let url = format!("https://api.telegram.org/bot{}/getUpdates", st.token);
let body = serde_json::json!({
"timeout": 30,
"offset": st.offset,
"allowed_updates": ["message", "edited_message", "callback_query", "message_reaction"],
});
let resp = st
.http
.post(&url)
.json(&body)
.timeout(std::time::Duration::from_secs(40))
.send()
.await;
let value: Value = match resp {
Ok(r) => match r.json().await {
Ok(v) => v,
Err(e) => {
tracing::warn!("Telegram raw poll: body read failed: {e}");
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
return;
}
},
Err(e) => {
tracing::warn!("Telegram raw poll: request failed: {e}");
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
return;
}
};
if value.get("ok").and_then(|v| v.as_bool()) != Some(true) {
tracing::warn!(
"Telegram raw poll: getUpdates not ok: {}",
crate::utils::truncate_str(&value.to_string(), 200)
);
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
return;
}
let Some(updates) = value.get("result").and_then(|v| v.as_array()) else {
return;
};
for u in updates {
let update_id = u.get("update_id").and_then(|v| v.as_i64()).unwrap_or(-1);
st.offset = st.offset.max(update_id + 1);
let mut u = u.clone();
for key in ["message", "edited_message"] {
if let Some(m) = u.get_mut(key) {
let chat_id = m
.get("chat")
.and_then(|c| c.get("id"))
.and_then(|v| v.as_i64());
let msg_id = m.get("message_id").and_then(|v| v.as_i64());
if let (Some(c), Some(mid)) = (chat_id, msg_id) {
stash_raw_message(c, mid as i32, m.clone());
}
synthesize_unknown_content(m);
}
}
match serde_json::from_str::<Update>(&u.to_string()) {
Ok(update) => {
tracing::debug!(
"Telegram raw poll: update {update_id} parsed, kind={}",
update_kind_name(&update)
);
st.pending.push_back(update);
}
Err(e) => {
tracing::error!(
"Telegram raw poll: update {update_id} failed typed parse ({e}) — raw \
stashed, typed dispatch skipped: {}",
crate::utils::truncate_str(&u.to_string(), 300)
);
}
}
}
if !updates.is_empty() {
tracing::info!(
"Telegram raw poll: batch of {} update(s), offset now {}",
updates.len(),
st.offset
);
}
}
const KNOWN_CONTENT_KEYS: &[&str] = &[
"text",
"caption",
"photo",
"voice",
"video",
"document",
"animation",
"video_note",
"sticker",
"audio",
"poll",
"location",
"contact",
"venue",
"dice",
"game",
"story",
"invoice",
"successful_payment",
"new_chat_members",
"left_chat_member",
"pinned_message",
"new_chat_title",
"new_chat_photo",
"delete_chat_photo",
"group_chat_created",
"supergroup_chat_created",
"channel_chat_created",
"message_auto_delete_timer_changed",
"migrate_to_chat_id",
"migrate_from_chat_id",
"forum_topic_created",
"forum_topic_closed",
"forum_topic_reopened",
"forum_topic_edited",
"video_chat_started",
"video_chat_ended",
"video_chat_scheduled",
"video_chat_participants_invited",
"web_app_data",
"proximity_alert_triggered",
"checklist",
"checklist_tasks_done",
"checklist_tasks_added",
"paid_media",
"giveaway",
"giveaway_created",
"giveaway_winners",
"giveaway_completed",
"gift",
"unique_gift",
"users_shared",
"chat_shared",
"refunded_payment",
"boost_added",
"chat_background_set",
"connected_website",
"write_access_allowed",
"passport_data",
"direct_message_price_changed",
"paid_message_price_changed",
"general_forum_topic_hidden",
"general_forum_topic_unhidden",
];
fn synthesize_unknown_content(m: &mut Value) {
let Some(obj) = m.as_object_mut() else { return };
if KNOWN_CONTENT_KEYS.iter().any(|k| obj.contains_key(*k)) {
return;
}
const ENVELOPE: &[&str] = &[
"message_id",
"from",
"chat",
"date",
"edit_date",
"forward_origin",
"forward_from",
"forward_date",
"reply_to_message",
"via_bot",
"sender_chat",
"message_thread_id",
"is_topic_message",
"has_protected_content",
"author_signature",
"entities",
"link_preview_options",
"quote",
"external_reply",
"effect_id",
"is_from_offline",
"business_connection_id",
"sender_boost_count",
"sender_business_bot",
"is_automatic_forward",
"paid_star_count",
];
let unknown: serde_json::Map<String, Value> = obj
.iter()
.filter(|(k, _)| !ENVELOPE.contains(&k.as_str()))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if unknown.is_empty() {
return;
}
let origin_note = raw_forward_origin(m)
.map(|o| format!(" forwarded from \"{o}\""))
.unwrap_or_default();
let text = match super::rich_decode::decode_rich_content(m) {
Some(decoded) => format!("[A rich message{origin_note}]:\n{decoded}"),
None => {
let pretty = serde_json::to_string_pretty(&Value::Object(unknown.clone()))
.unwrap_or_else(|_| "<unrenderable>".to_string());
format!(
"[A message{origin_note} arrived in a format the Bot API client cannot \
decode natively. Its raw content payload follows — read the content \
directly from it:]\n```json\n{}\n```",
crate::utils::truncate_str(&pretty, 4096)
)
}
};
tracing::warn!(
"Telegram raw poll: synthesizing text for unknown content keys {:?} (chat={:?}, msg={:?})",
unknown.keys().collect::<Vec<_>>(),
m.get("chat").and_then(|c| c.get("id")),
m.get("message_id"),
);
let obj = m.as_object_mut().expect("checked above");
for k in unknown.keys() {
obj.remove(k);
}
obj.remove("entities");
obj.insert("text".to_string(), Value::String(text));
}
fn update_kind_name(u: &Update) -> &'static str {
use teloxide::types::UpdateKind;
match &u.kind {
UpdateKind::Message(_) => "message",
UpdateKind::EditedMessage(_) => "edited_message",
UpdateKind::CallbackQuery(_) => "callback_query",
UpdateKind::MessageReaction(_) => "message_reaction",
UpdateKind::Error(_) => "ERROR(unparsed)",
_ => "other",
}
}
fn raw_update_stream(
st: &mut RawPollState,
) -> impl futures::Stream<Item = Result<Update, std::convert::Infallible>> + Send + '_ {
futures::stream::unfold(st, |st| async move {
loop {
if st.flag.is_stopped() {
return None;
}
if let Some(u) = st.pending.pop_front() {
return Some((Ok(u), st));
}
poll_once(st).await;
}
})
}
fn raw_stop_token(st: &mut RawPollState) -> StopToken {
st.stop_token.clone()
}
pub(crate) fn raw_polling_listener(
token: String,
) -> impl teloxide::update_listeners::UpdateListener<Err = std::convert::Infallible> {
let (stop_token, flag) = mk_stop_token();
let state = RawPollState {
http: reqwest::Client::new(),
token,
offset: 0,
pending: VecDeque::new(),
flag,
stop_token,
};
StatefulListener::new(state, raw_update_stream, raw_stop_token)
}