Skip to main content

layer_client/
update.rs

1//! High-level update types delivered by [`crate::Client::stream_updates`].
2//!
3//! Every update the Telegram server pushes is classified into one of the
4//! variants of [`Update`].  The raw constructor ID is always available
5//! via [`Update::Raw`] for anything not yet wrapped.
6
7use layer_tl_types as tl;
8use layer_tl_types::{Cursor, Deserializable};
9
10use crate::{Client, InvocationError as Error};
11
12// ─── IncomingMessage ─────────────────────────────────────────────────────────
13
14/// A new or edited message.
15#[derive(Debug, Clone)]
16pub struct IncomingMessage {
17    /// The underlying TL message object.
18    pub raw: tl::enums::Message,
19}
20
21impl IncomingMessage {
22    pub fn from_raw(raw: tl::enums::Message) -> Self {
23        Self { raw }
24    }
25
26    /// The message text (or caption for media messages).
27    pub fn text(&self) -> Option<&str> {
28        match &self.raw {
29            tl::enums::Message::Message(m) => {
30                if m.message.is_empty() { None } else { Some(&m.message) }
31            }
32            _ => None,
33        }
34    }
35
36    /// Unique message ID within the chat.
37    pub fn id(&self) -> i32 {
38        match &self.raw {
39            tl::enums::Message::Message(m) => m.id,
40            tl::enums::Message::Service(m) => m.id,
41            tl::enums::Message::Empty(m)   => m.id,
42        }
43    }
44
45    /// The peer (chat) this message belongs to.
46    pub fn peer_id(&self) -> Option<&tl::enums::Peer> {
47        match &self.raw {
48            tl::enums::Message::Message(m) => Some(&m.peer_id),
49            tl::enums::Message::Service(m) => Some(&m.peer_id),
50            _ => None,
51        }
52    }
53
54    /// The sender peer, if available (not set for anonymous channel posts).
55    pub fn sender_id(&self) -> Option<&tl::enums::Peer> {
56        match &self.raw {
57            tl::enums::Message::Message(m) => m.from_id.as_ref(),
58            tl::enums::Message::Service(m) => m.from_id.as_ref(),
59            _ => None,
60        }
61    }
62
63    /// `true` if the message was sent by the logged-in account.
64    pub fn outgoing(&self) -> bool {
65        match &self.raw {
66            tl::enums::Message::Message(m) => m.out,
67            tl::enums::Message::Service(m) => m.out,
68            _ => false,
69        }
70    }
71
72    /// Unix timestamp when the message was sent.
73    pub fn date(&self) -> i32 {
74        match &self.raw {
75            tl::enums::Message::Message(m) => m.date,
76            tl::enums::Message::Service(m) => m.date,
77            _ => 0,
78        }
79    }
80
81    /// Unix timestamp of the last edit, if the message has been edited.
82    pub fn edit_date(&self) -> Option<i32> {
83        match &self.raw {
84            tl::enums::Message::Message(m) => m.edit_date,
85            _ => None,
86        }
87    }
88
89    /// `true` if the logged-in user was mentioned in this message.
90    pub fn mentioned(&self) -> bool {
91        match &self.raw {
92            tl::enums::Message::Message(m) => m.mentioned,
93            tl::enums::Message::Service(m) => m.mentioned,
94            _ => false,
95        }
96    }
97
98    /// `true` if the message was sent silently (no notification).
99    pub fn silent(&self) -> bool {
100        match &self.raw {
101            tl::enums::Message::Message(m) => m.silent,
102            tl::enums::Message::Service(m) => m.silent,
103            _ => false,
104        }
105    }
106
107    /// `true` if this is a channel post (no sender).
108    pub fn post(&self) -> bool {
109        match &self.raw {
110            tl::enums::Message::Message(m) => m.post,
111            _ => false,
112        }
113    }
114
115    /// `true` if this message is currently pinned.
116    pub fn pinned(&self) -> bool {
117        match &self.raw {
118            tl::enums::Message::Message(m) => m.pinned,
119            _ => false,
120        }
121    }
122
123    /// Number of times the message has been forwarded (channels only).
124    pub fn forward_count(&self) -> Option<i32> {
125        match &self.raw {
126            tl::enums::Message::Message(m) => m.forwards,
127            _ => None,
128        }
129    }
130
131    /// View count for channel posts.
132    pub fn view_count(&self) -> Option<i32> {
133        match &self.raw {
134            tl::enums::Message::Message(m) => m.views,
135            _ => None,
136        }
137    }
138
139    /// Reply count (number of replies in a thread).
140    pub fn reply_count(&self) -> Option<i32> {
141        match &self.raw {
142            tl::enums::Message::Message(m) => {
143                m.replies.as_ref().map(|r| match r {
144                    tl::enums::MessageReplies::MessageReplies(x) => x.replies,
145                })
146            }
147            _ => None,
148        }
149    }
150
151    /// ID of the message this one is replying to.
152    pub fn reply_to_message_id(&self) -> Option<i32> {
153        match &self.raw {
154            tl::enums::Message::Message(m) => {
155                m.reply_to.as_ref().and_then(|r| match r {
156                    tl::enums::MessageReplyHeader::MessageReplyHeader(h) => h.reply_to_msg_id,
157                    _ => None,
158                })
159            }
160            _ => None,
161        }
162    }
163
164    /// Fetch the message that this one is replying to.
165    ///
166    /// Returns `None` if this message is not a reply or if the peer is unknown.
167    /// Unlike [`reply_to_message_id`] this actually performs an API call to
168    /// retrieve the full message object.
169    ///
170    /// [`reply_to_message_id`]: IncomingMessage::reply_to_message_id
171    pub async fn reply_to_message(
172        &self,
173        client: &Client,
174    ) -> Result<Option<IncomingMessage>, Error> {
175        let reply_id = match self.reply_to_message_id() {
176            Some(id) => id,
177            None     => return Ok(None),
178        };
179        let peer = match self.peer_id() {
180            Some(p) => p.clone(),
181            None    => return Ok(None),
182        };
183        let msgs = client.get_messages_by_id(peer, &[reply_id]).await?;
184        Ok(msgs.into_iter().next())
185    }
186
187    /// The message's send time as a [`chrono::DateTime<chrono::Utc>`].
188    ///
189    /// This is a typed wrapper around the raw `date()` Unix timestamp.
190    pub fn date_utc(&self) -> Option<chrono::DateTime<chrono::Utc>> {
191        use chrono::TimeZone;
192        let ts = self.date();
193        if ts == 0 { return None; }
194        chrono::Utc.timestamp_opt(ts as i64, 0).single()
195    }
196
197    /// The last edit time as a [`chrono::DateTime<chrono::Utc>`], if edited.
198    pub fn edit_date_utc(&self) -> Option<chrono::DateTime<chrono::Utc>> {
199        use chrono::TimeZone;
200        self.edit_date().and_then(|ts| chrono::Utc.timestamp_opt(ts as i64, 0).single())
201    }
202
203    /// The media attached to this message, if any.
204    pub fn media(&self) -> Option<&tl::enums::MessageMedia> {
205        match &self.raw {
206            tl::enums::Message::Message(m) => m.media.as_ref(),
207            _ => None,
208        }
209    }
210
211    /// Formatting entities (bold, italic, code, links, etc).
212    pub fn entities(&self) -> Option<&Vec<tl::enums::MessageEntity>> {
213        match &self.raw {
214            tl::enums::Message::Message(m) => m.entities.as_ref(),
215            _ => None,
216        }
217    }
218
219    /// Group ID for album messages (multiple media in one).
220    pub fn grouped_id(&self) -> Option<i64> {
221        match &self.raw {
222            tl::enums::Message::Message(m) => m.grouped_id,
223            _ => None,
224        }
225    }
226
227    /// `true` if this message was sent from a scheduled one.
228    pub fn from_scheduled(&self) -> bool {
229        match &self.raw {
230            tl::enums::Message::Message(m) => m.from_scheduled,
231            _ => false,
232        }
233    }
234
235    /// `true` if the edit date is hidden from recipients.
236    pub fn edit_hide(&self) -> bool {
237        match &self.raw {
238            tl::enums::Message::Message(m) => m.edit_hide,
239            _ => false,
240        }
241    }
242
243    /// `true` if the media in this message has not been read yet.
244    pub fn media_unread(&self) -> bool {
245        match &self.raw {
246            tl::enums::Message::Message(m) => m.media_unread,
247            tl::enums::Message::Service(m) => m.media_unread,
248            _ => false,
249        }
250    }
251
252    /// ID of the bot that sent this message via inline mode, if any.
253    pub fn via_bot_id(&self) -> Option<i64> {
254        match &self.raw {
255            tl::enums::Message::Message(m) => m.via_bot_id,
256            _ => None,
257        }
258    }
259
260    /// Signature of the post author in a channel, if set.
261    pub fn post_author(&self) -> Option<&str> {
262        match &self.raw {
263            tl::enums::Message::Message(m) => m.post_author.as_deref(),
264            _ => None,
265        }
266    }
267
268    /// Number of reactions on this message, if any.
269    pub fn reaction_count(&self) -> i32 {
270        match &self.raw {
271            tl::enums::Message::Message(m) => {
272                m.reactions.as_ref().map(|r| match r {
273                    tl::enums::MessageReactions::MessageReactions(x) => {
274                        x.results.iter().map(|res| match res {
275                            tl::enums::ReactionCount::ReactionCount(c) => c.count,
276                        }).sum()
277                    }
278                }).unwrap_or(0)
279            }
280            _ => 0,
281        }
282    }
283
284    /// Restriction reasons (why this message is unavailable in some regions).
285    pub fn restriction_reason(&self) -> Option<&Vec<tl::enums::RestrictionReason>> {
286        match &self.raw {
287            tl::enums::Message::Message(m) => m.restriction_reason.as_ref(),
288            _ => None,
289        }
290    }
291
292    /// Reply markup (inline keyboards, etc).
293    pub fn reply_markup(&self) -> Option<&tl::enums::ReplyMarkup> {
294        match &self.raw {
295            tl::enums::Message::Message(m) => m.reply_markup.as_ref(),
296            _ => None,
297        }
298    }
299
300    /// Forward info header, if this message was forwarded.
301    pub fn forward_header(&self) -> Option<&tl::enums::MessageFwdHeader> {
302        match &self.raw {
303            tl::enums::Message::Message(m) => m.fwd_from.as_ref(),
304            _ => None,
305        }
306    }
307
308    /// `true` if forwarding this message is restricted.
309    pub fn noforwards(&self) -> bool {
310        match &self.raw {
311            tl::enums::Message::Message(m) => m.noforwards,
312            _ => false,
313        }
314    }
315
316    /// Reconstruct Markdown from the message text and its formatting entities.
317    ///
318    /// Returns plain text if there are no entities.
319    pub fn markdown_text(&self) -> Option<String> {
320        let text = self.text()?;
321        let entities = self.entities().map(|e| e.as_slice()).unwrap_or(&[]);
322        Some(crate::parsers::generate_markdown(text, entities))
323    }
324
325    /// Reconstruct HTML from the message text and its formatting entities.
326    ///
327    /// Returns plain text if there are no entities.
328    pub fn html_text(&self) -> Option<String> {
329        let text = self.text()?;
330        let entities = self.entities().map(|e| e.as_slice()).unwrap_or(&[]);
331        Some(crate::parsers::generate_html(text, entities))
332    }
333
334    /// Reply to this message with plain text.
335    pub async fn reply(&self, client: &mut Client, text: impl Into<String>) -> Result<(), Error> {
336        let peer = match self.peer_id() {
337            Some(p) => p.clone(),
338            None    => return Err(Error::Deserialize("cannot reply: unknown peer".into())),
339        };
340        let msg_id = self.id();
341        client.send_message_to_peer_ex(peer, &crate::InputMessage::text(text.into())
342            .reply_to(Some(msg_id))).await
343    }
344}
345
346// ─── MessageDeletion ─────────────────────────────────────────────────────────
347
348/// One or more messages were deleted.
349#[derive(Debug, Clone)]
350pub struct MessageDeletion {
351    /// IDs of the deleted messages.
352    pub message_ids: Vec<i32>,
353    /// Channel ID, if the deletion happened in a channel / supergroup.
354    pub channel_id:  Option<i64>,
355}
356
357impl MessageDeletion {
358    /// Consume self and return the deleted message IDs without cloning.
359    pub fn into_messages(self) -> Vec<i32> {
360        self.message_ids
361    }
362}
363
364// ─── CallbackQuery ───────────────────────────────────────────────────────────
365
366/// A user pressed an inline keyboard button on a bot message.
367#[derive(Debug, Clone)]
368pub struct CallbackQuery {
369    pub query_id:        i64,
370    pub user_id:         i64,
371    pub message_id:      Option<i32>,
372    pub chat_instance:   i64,
373    /// Raw `data` bytes from the button.
374    pub data_raw:        Option<Vec<u8>>,
375    /// Game short name (if a game button was pressed).
376    pub game_short_name: Option<String>,
377    /// G-38: The peer (chat/channel/user) where the button was pressed.
378    /// `None` for inline-message callback queries.
379    pub chat_peer:       Option<tl::enums::Peer>,
380    /// G-38: For inline-message callbacks — the message ID token.
381    pub inline_msg_id:   Option<tl::enums::InputBotInlineMessageId>,
382}
383
384impl CallbackQuery {
385    /// Button data as a UTF-8 string, if valid.
386    pub fn data(&self) -> Option<&str> {
387        self.data_raw.as_ref().and_then(|d| std::str::from_utf8(d).ok())
388    }
389
390    /// Answer the callback query (removes the loading indicator on the client).
391    pub async fn answer(
392        &self,
393        client: &mut Client,
394        text:   Option<&str>,
395    ) -> Result<(), Error> {
396        client.answer_callback_query(self.query_id, text, false).await.map(|_| ())
397    }
398
399    /// Answer with a popup alert.
400    pub async fn answer_alert(
401        &self,
402        client: &mut Client,
403        text:   &str,
404    ) -> Result<(), Error> {
405        client.answer_callback_query(self.query_id, Some(text), true).await.map(|_| ())
406    }
407}
408
409// ─── InlineQuery ─────────────────────────────────────────────────────────────
410
411/// A user is typing an inline query (`@bot something`).
412#[derive(Debug, Clone)]
413pub struct InlineQuery {
414    pub query_id: i64,
415    pub user_id:  i64,
416    pub query:    String,
417    pub offset:   String,
418    /// Peer of the chat the user sent the inline query from, if available.
419    pub peer:     Option<tl::enums::Peer>,
420}
421
422impl InlineQuery {
423    /// The text the user typed after the bot username.
424    pub fn query(&self) -> &str { &self.query }
425}
426
427// ─── InlineSend ──────────────────────────────────────────────────────────────
428
429/// A user chose an inline result and sent it.
430#[derive(Debug, Clone)]
431pub struct InlineSend {
432    pub user_id:  i64,
433    pub query:    String,
434    pub id:       String,
435    /// Message ID of the sent message, if available.
436    pub msg_id:   Option<tl::enums::InputBotInlineMessageId>,
437}
438
439impl InlineSend {
440    /// G-39: Edit the inline message that was sent as a result of this inline query.
441    ///
442    /// Requires that [`msg_id`] is present (i.e. the result had `peer_type` set).
443    /// Returns `Err` with a descriptive message if `msg_id` is `None`.
444    ///
445    /// [`msg_id`]: InlineSend::msg_id
446    ///
447    /// # Example
448    /// ```rust,no_run
449    /// # async fn f(client: layer_client::Client, send: layer_client::update::InlineSend)
450    /// # -> Result<(), Box<dyn std::error::Error>> {
451    /// send.edit_message(&client, "updated text", None).await?;
452    /// # Ok(()) }
453    /// ```
454    pub async fn edit_message(
455        &self,
456        client:       &Client,
457        new_text:     &str,
458        reply_markup: Option<tl::enums::ReplyMarkup>,
459    ) -> Result<bool, Error> {
460        let msg_id = match self.msg_id.clone() {
461            Some(id) => id,
462            None => return Err(Error::Deserialize(
463                "InlineSend::edit_message — msg_id is None (bot_inline_send had no peer_type)".into()
464            )),
465        };
466        let req = tl::functions::messages::EditInlineBotMessage {
467            no_webpage:   false,
468            invert_media: false,
469            id:           msg_id,
470            message:      Some(new_text.to_string()),
471            media:        None,
472            reply_markup,
473            entities:     None,
474        };
475        let body = client.rpc_call_raw(&req).await?;
476        // Returns Bool
477        Ok(!body.is_empty())
478    }
479}
480
481// ─── RawUpdate ───────────────────────────────────────────────────────────────
482
483/// A TL update that has no dedicated high-level variant yet.
484#[derive(Debug, Clone)]
485pub struct RawUpdate {
486    /// Constructor ID of the inner update.
487    pub constructor_id: u32,
488}
489
490/// A high-level event received from Telegram.
491#[non_exhaustive]
492#[derive(Debug, Clone)]
493pub enum Update {
494    /// A new message (personal chat, group, channel, or bot command).
495    NewMessage(IncomingMessage),
496    /// An existing message was edited.
497    MessageEdited(IncomingMessage),
498    /// One or more messages were deleted.
499    MessageDeleted(MessageDeletion),
500    /// An inline keyboard button was pressed on a bot message.
501    CallbackQuery(CallbackQuery),
502    /// A user typed an inline query for the bot.
503    InlineQuery(InlineQuery),
504    /// A user chose an inline result and sent it (bots only).
505    InlineSend(InlineSend),
506    /// A raw TL update not mapped to any of the above variants.
507    Raw(RawUpdate),
508}
509
510// ─── MTProto update container IDs ────────────────────────────────────────────
511
512const ID_UPDATES_TOO_LONG:      u32 = 0xe317af7e;
513const ID_UPDATE_SHORT_MESSAGE:  u32 = 0x313bc7f8;
514const ID_UPDATE_SHORT_CHAT_MSG: u32 = 0x4d6deea5;
515const ID_UPDATE_SHORT:          u32 = 0x78d4dec1;
516const ID_UPDATES:               u32 = 0x74ae4240;
517const ID_UPDATES_COMBINED:      u32 = 0x725b04c3;
518
519// ─── Parser ──────────────────────────────────────────────────────────────────
520
521/// Parse raw update container bytes into high-level [`Update`] values.
522pub(crate) fn parse_updates(bytes: &[u8]) -> Vec<Update> {
523    if bytes.len() < 4 {
524        return vec![];
525    }
526    let cid = u32::from_le_bytes(bytes[..4].try_into().unwrap());
527
528    match cid {
529        ID_UPDATES_TOO_LONG => {
530            log::warn!("[layer] updatesTooLong — call client.get_difference() to recover missed updates");
531            vec![]
532        }
533
534        ID_UPDATE_SHORT_MESSAGE => {
535            let mut cur = Cursor::from_slice(&bytes[4..]); // skip constructor prefix
536            match tl::types::UpdateShortMessage::deserialize(&mut cur) {
537                Ok(m)  => vec![Update::NewMessage(make_short_dm(m))],
538                Err(e) => { log::debug!("[layer] updateShortMessage parse error (unknown constructor or newer layer): {e}"); vec![] }
539            }
540        }
541
542        ID_UPDATE_SHORT_CHAT_MSG => {
543            let mut cur = Cursor::from_slice(&bytes[4..]); // skip constructor prefix
544            match tl::types::UpdateShortChatMessage::deserialize(&mut cur) {
545                Ok(m)  => vec![Update::NewMessage(make_short_chat(m))],
546                Err(e) => { log::debug!("[layer] updateShortChatMessage parse error (unknown constructor or newer layer): {e}"); vec![] }
547            }
548        }
549
550        ID_UPDATE_SHORT => {
551            let mut cur = Cursor::from_slice(&bytes[4..]); // skip constructor prefix
552            match tl::types::UpdateShort::deserialize(&mut cur) {
553                Ok(m)  => from_single_update(m.update),
554                Err(e) => { log::debug!("[layer] updateShort parse error (unknown constructor or newer layer): {e}"); vec![] }
555            }
556        }
557
558        ID_UPDATES => {
559            let mut cur = Cursor::from_slice(bytes);
560            match tl::enums::Updates::deserialize(&mut cur) {
561                Ok(tl::enums::Updates::Updates(u)) => {
562                    u.updates.into_iter().flat_map(from_single_update).collect()
563                }
564                Err(e) => { log::debug!("[layer] Updates parse error (unknown constructor or newer layer): {e}"); vec![] }
565                _ => vec![],
566            }
567        }
568
569        ID_UPDATES_COMBINED => {
570            let mut cur = Cursor::from_slice(bytes);
571            match tl::enums::Updates::deserialize(&mut cur) {
572                Ok(tl::enums::Updates::Combined(u)) => {
573                    u.updates.into_iter().flat_map(from_single_update).collect()
574                }
575                Err(e) => { log::debug!("[layer] UpdatesCombined parse error (unknown constructor or newer layer): {e}"); vec![] }
576                _ => vec![],
577            }
578        }
579
580        _ => vec![],
581    }
582}
583
584/// Convert a single `tl::enums::Update` into a `Vec<Update>`.
585pub fn from_single_update_pub(upd: tl::enums::Update) -> Vec<Update> {
586    from_single_update(upd)
587}
588
589/// Convert a single `tl::enums::Update` into a `Vec<Update>`.
590fn from_single_update(upd: tl::enums::Update) -> Vec<Update> {
591    use tl::enums::Update::*;
592    match upd {
593        NewMessage(u) => vec![Update::NewMessage(IncomingMessage::from_raw(u.message))],
594        NewChannelMessage(u) => vec![Update::NewMessage(IncomingMessage::from_raw(u.message))],
595        EditMessage(u) => vec![Update::MessageEdited(IncomingMessage::from_raw(u.message))],
596        EditChannelMessage(u) => vec![Update::MessageEdited(IncomingMessage::from_raw(u.message))],
597        DeleteMessages(u) => vec![Update::MessageDeleted(MessageDeletion {
598            message_ids: u.messages,
599            channel_id: None,
600        })],
601        DeleteChannelMessages(u) => vec![Update::MessageDeleted(MessageDeletion {
602            message_ids: u.messages,
603            channel_id: Some(u.channel_id),
604        })],
605        BotCallbackQuery(u) => vec![Update::CallbackQuery(CallbackQuery {
606            query_id:        u.query_id,
607            user_id:         u.user_id,
608            message_id:      Some(u.msg_id),
609            chat_instance:   u.chat_instance,
610            data_raw:        u.data,
611            game_short_name: u.game_short_name,
612            chat_peer:       Some(u.peer),
613            inline_msg_id:   None,
614        })],
615        InlineBotCallbackQuery(u) => vec![Update::CallbackQuery(CallbackQuery {
616            query_id:        u.query_id,
617            user_id:         u.user_id,
618            message_id:      None,
619            chat_instance:   u.chat_instance,
620            data_raw:        u.data,
621            game_short_name: u.game_short_name,
622            chat_peer:       None,
623            inline_msg_id:   Some(u.msg_id),
624        })],
625        BotInlineQuery(u) => vec![Update::InlineQuery(InlineQuery {
626            query_id: u.query_id,
627            user_id:  u.user_id,
628            query:    u.query,
629            offset:   u.offset,
630            peer:     None,
631        })],
632        BotInlineSend(u) => vec![Update::InlineSend(InlineSend {
633            user_id: u.user_id,
634            query:   u.query,
635            id:      u.id,
636            msg_id:  u.msg_id,
637        })],
638        other => {
639            let cid = tl_constructor_id(&other);
640            vec![Update::Raw(RawUpdate { constructor_id: cid })]
641        }
642    }
643}
644
645/// Extract constructor ID from a `tl::enums::Update` variant.
646fn tl_constructor_id(upd: &tl::enums::Update) -> u32 {
647    use tl::enums::Update::*;
648    match upd {
649        AttachMenuBots => 0x17b7a20b,
650        AutoSaveSettings => 0xec05b097,
651        BotBusinessConnect(_) => 0x8ae5c97a,
652        BotCallbackQuery(_) => 0xb9cfc48d,
653        BotChatBoost(_) => 0x904dd49c,
654        BotChatInviteRequester(_) => 0x11dfa986,
655        BotCommands(_) => 0x4d712f2e,
656        BotDeleteBusinessMessage(_) => 0xa02a982e,
657        BotEditBusinessMessage(_) => 0x7df587c,
658        BotInlineQuery(_) => 0x496f379c,
659        BotInlineSend(_) => 0x12f12a07,
660        BotMenuButton(_) => 0x14b85813,
661        BotMessageReaction(_) => 0xac21d3ce,
662        BotMessageReactions(_) => 0x9cb7759,
663        BotNewBusinessMessage(_) => 0x9ddb347c,
664        BotPrecheckoutQuery(_) => 0x8caa9a96,
665        BotPurchasedPaidMedia(_) => 0x283bd312,
666        BotShippingQuery(_) => 0xb5aefd7d,
667        BotStopped(_) => 0xc4870a49,
668        BotWebhookJson(_) => 0x8317c0c3,
669        BotWebhookJsonquery(_) => 0x9b9240a6,
670        BusinessBotCallbackQuery(_) => 0x1ea2fda7,
671        Channel(_) => 0x635b4c09,
672        ChannelAvailableMessages(_) => 0xb23fc698,
673        ChannelMessageForwards(_) => 0xd29a27f4,
674        ChannelMessageViews(_) => 0xf226ac08,
675        ChannelParticipant(_) => 0x985d3abb,
676        ChannelReadMessagesContents(_) => 0x25f324f7,
677        ChannelTooLong(_) => 0x108d941f,
678        ChannelUserTyping(_) => 0x8c88c923,
679        ChannelViewForumAsMessages(_) => 0x7b68920,
680        ChannelWebPage(_) => 0x2f2ba99f,
681        Chat(_) => 0xf89a6a4e,
682        ChatDefaultBannedRights(_) => 0x54c01850,
683        ChatParticipant(_) => 0xd087663a,
684        ChatParticipantAdd(_) => 0x3dda5451,
685        ChatParticipantAdmin(_) => 0xd7ca61a2,
686        ChatParticipantDelete(_) => 0xe32f3d77,
687        ChatParticipants(_) => 0x7761198,
688        ChatUserTyping(_) => 0x83487af0,
689        Config => 0xa229dd06,
690        ContactsReset => 0x7084a7be,
691        DcOptions(_) => 0x8e5e9873,
692        DeleteChannelMessages(_) => 0xc32d5b12,
693        DeleteGroupCallMessages(_) => 0x3e85e92c,
694        DeleteMessages(_) => 0xa20db0e5,
695        DeleteQuickReply(_) => 0x53e6f1ec,
696        DeleteQuickReplyMessages(_) => 0x566fe7cd,
697        DeleteScheduledMessages(_) => 0xf2a71983,
698        DialogFilter(_) => 0x26ffde7d,
699        DialogFilterOrder(_) => 0xa5d72105,
700        DialogFilters => 0x3504914f,
701        DialogPinned(_) => 0x6e6fe51c,
702        DialogUnreadMark(_) => 0xb658f23e,
703        DraftMessage(_) => 0xedfc111e,
704        EditChannelMessage(_) => 0x1b3f4df7,
705        EditMessage(_) => 0xe40370a3,
706        EmojiGameInfo(_) => 0xfb9c547a,
707        EncryptedChatTyping(_) => 0x1710f156,
708        EncryptedMessagesRead(_) => 0x38fe25b7,
709        Encryption(_) => 0xb4a2e88d,
710        FavedStickers => 0xe511996d,
711        FolderPeers(_) => 0x19360dc0,
712        GeoLiveViewed(_) => 0x871fb939,
713        GroupCall(_) => 0x9d2216e0,
714        GroupCallChainBlocks(_) => 0xa477288f,
715        GroupCallConnection(_) => 0xb783982,
716        GroupCallEncryptedMessage(_) => 0xc957a766,
717        GroupCallMessage(_) => 0xd8326f0d,
718        GroupCallParticipants(_) => 0xf2ebdb4e,
719        InlineBotCallbackQuery(_) => 0x691e9052,
720        LangPack(_) => 0x56022f4d,
721        LangPackTooLong(_) => 0x46560264,
722        LoginToken => 0x564fe691,
723        MessageExtendedMedia(_) => 0xd5a41724,
724        MessageId(_) => 0x4e90bfd6,
725        MessagePoll(_) => 0xaca1657b,
726        MessagePollVote(_) => 0x24f40e77,
727        MessageReactions(_) => 0x1e297bfa,
728        MonoForumNoPaidException(_) => 0x9f812b08,
729        MoveStickerSetToTop(_) => 0x86fccf85,
730        NewAuthorization(_) => 0x8951abef,
731        NewChannelMessage(_) => 0x62ba04d9,
732        NewEncryptedMessage(_) => 0x12bcbd9a,
733        NewMessage(_) => 0x1f2b0afd,
734        NewQuickReply(_) => 0xf53da717,
735        NewScheduledMessage(_) => 0x39a51dfb,
736        NewStickerSet(_) => 0x688a30aa,
737        NewStoryReaction(_) => 0x1824e40b,
738        NotifySettings(_) => 0xbec268ef,
739        PaidReactionPrivacy(_) => 0x8b725fce,
740        PeerBlocked(_) => 0xebe07752,
741        PeerHistoryTtl(_) => 0xbb9bb9a5,
742        PeerLocated(_) => 0xb4afcfb0,
743        PeerSettings(_) => 0x6a7e7366,
744        PeerWallpaper(_) => 0xae3f101d,
745        PendingJoinRequests(_) => 0x7063c3db,
746        PhoneCall(_) => 0xab0f6b1e,
747        PhoneCallSignalingData(_) => 0x2661bf09,
748        PinnedChannelMessages(_) => 0x5bb98608,
749        PinnedDialogs(_) => 0xfa0f3ca2,
750        PinnedForumTopic(_) => 0x683b2c52,
751        PinnedForumTopics(_) => 0xdef143d0,
752        PinnedMessages(_) => 0xed85eab5,
753        PinnedSavedDialogs(_) => 0x686c85a6,
754        Privacy(_) => 0xee3b272a,
755        PtsChanged => 0x3354678f,
756        QuickReplies(_) => 0xf9470ab2,
757        QuickReplyMessage(_) => 0x3e050d0f,
758        ReadChannelDiscussionInbox(_) => 0xd6b19546,
759        ReadChannelDiscussionOutbox(_) => 0x695c9e7c,
760        ReadChannelInbox(_) => 0x922e6e10,
761        ReadChannelOutbox(_) => 0xb75f99a9,
762        ReadFeaturedEmojiStickers => 0xfb4c496c,
763        ReadFeaturedStickers => 0x571d2742,
764        ReadHistoryInbox(_) => 0x9e84bc99,
765        ReadHistoryOutbox(_) => 0x2f2f21bf,
766        ReadMessagesContents(_) => 0xf8227181,
767        ReadMonoForumInbox(_) => 0x77b0e372,
768        ReadMonoForumOutbox(_) => 0xa4a79376,
769        ReadStories(_) => 0xf74e932b,
770        RecentEmojiStatuses => 0x30f443db,
771        RecentReactions => 0x6f7863f4,
772        RecentStickers => 0x9a422c20,
773        SavedDialogPinned(_) => 0xaeaf9e74,
774        SavedGifs => 0x9375341e,
775        SavedReactionTags => 0x39c67432,
776        SavedRingtones => 0x74d8be99,
777        SentPhoneCode(_) => 0x504aa18f,
778        SentStoryReaction(_) => 0x7d627683,
779        ServiceNotification(_) => 0xebe46819,
780        SmsJob(_) => 0xf16269d4,
781        StarGiftAuctionState(_) => 0x48e246c2,
782        StarGiftAuctionUserState(_) => 0xdc58f31e,
783        StarGiftCraftFail => 0xac072444,
784        StarsBalance(_) => 0x4e80a379,
785        StarsRevenueStatus(_) => 0xa584b019,
786        StickerSets(_) => 0x31c24808,
787        StickerSetsOrder(_) => 0xbb2d201,
788        StoriesStealthMode(_) => 0x2c084dc1,
789        Story(_) => 0x75b3b798,
790        StoryId(_) => 0x1bf335b9,
791        Theme(_) => 0x8216fba3,
792        TranscribedAudio(_) => 0x84cd5a,
793        User(_) => 0x20529438,
794        UserEmojiStatus(_) => 0x28373599,
795        UserName(_) => 0xa7848924,
796        UserPhone(_) => 0x5492a13,
797        UserStatus(_) => 0xe5bdf8de,
798        UserTyping(_) => 0x2a17bf5c,
799        WebPage(_) => 0x7f891213,
800        WebViewResultSent(_) => 0x1592b79d,
801        ChatParticipantRank(_) => 0xbd8367b9,
802        ManagedBot(_) => 0x4880ed9a,
803    }
804}
805
806// ─── Short message helpers ────────────────────────────────────────────────────
807
808fn make_short_dm(m: tl::types::UpdateShortMessage) -> IncomingMessage {
809    let msg = tl::types::Message {
810        out:               m.out,
811        mentioned:         m.mentioned,
812        media_unread:      m.media_unread,
813        silent:            m.silent,
814        post:              false,
815        from_scheduled:    false,
816        legacy:            false,
817        edit_hide:         false,
818        pinned:            false,
819        noforwards:        false,
820        invert_media:      false,
821        offline:           false,
822        video_processing_pending: false,
823        id:                m.id,
824        from_id:           Some(tl::enums::Peer::User(tl::types::PeerUser { user_id: m.user_id })),
825        peer_id:           tl::enums::Peer::User(tl::types::PeerUser { user_id: m.user_id }),
826        saved_peer_id:     None,
827        fwd_from:          m.fwd_from,
828        via_bot_id:        m.via_bot_id,
829        via_business_bot_id: None,
830        reply_to:          m.reply_to,
831        date:              m.date,
832        message:           m.message,
833        media:             None,
834        reply_markup:      None,
835        entities:          m.entities,
836        views:             None,
837        forwards:          None,
838        replies:           None,
839        edit_date:         None,
840        post_author:       None,
841        grouped_id:        None,
842        reactions:         None,
843        restriction_reason: None,
844        ttl_period:        None,
845        quick_reply_shortcut_id: None,
846        effect:            None,
847        factcheck:         None,
848        report_delivery_until_date: None,
849        paid_message_stars: None,
850        suggested_post:    None,
851        from_rank:           None,
852        from_boosts_applied: None,
853        paid_suggested_post_stars: false,
854        paid_suggested_post_ton: false,
855        schedule_repeat_period: None,
856        summary_from_language: None,
857    };
858    IncomingMessage { raw: tl::enums::Message::Message(msg) }
859}
860
861fn make_short_chat(m: tl::types::UpdateShortChatMessage) -> IncomingMessage {
862    let msg = tl::types::Message {
863        out:               m.out,
864        mentioned:         m.mentioned,
865        media_unread:      m.media_unread,
866        silent:            m.silent,
867        post:              false,
868        from_scheduled:    false,
869        legacy:            false,
870        edit_hide:         false,
871        pinned:            false,
872        noforwards:        false,
873        invert_media:      false,
874        offline:           false,
875        video_processing_pending: false,
876        id:                m.id,
877        from_id:           Some(tl::enums::Peer::User(tl::types::PeerUser { user_id: m.from_id })),
878        peer_id:           tl::enums::Peer::Chat(tl::types::PeerChat { chat_id: m.chat_id }),
879        saved_peer_id:     None,
880        fwd_from:          m.fwd_from,
881        via_bot_id:        m.via_bot_id,
882        via_business_bot_id: None,
883        reply_to:          m.reply_to,
884        date:              m.date,
885        message:           m.message,
886        media:             None,
887        reply_markup:      None,
888        entities:          m.entities,
889        views:             None,
890        forwards:          None,
891        replies:           None,
892        edit_date:         None,
893        post_author:       None,
894        grouped_id:        None,
895        reactions:         None,
896        restriction_reason: None,
897        ttl_period:        None,
898        quick_reply_shortcut_id: None,
899        effect:            None,
900        factcheck:         None,
901        report_delivery_until_date: None,
902        paid_message_stars: None,
903        suggested_post:    None,
904        from_rank:           None,
905        from_boosts_applied: None,
906        paid_suggested_post_stars: false,
907        paid_suggested_post_ton: false,
908        schedule_repeat_period: None,
909        summary_from_language: None,
910    };
911    IncomingMessage { raw: tl::enums::Message::Message(msg) }
912}