Skip to main content

layer_client/
update.rs

1//! High-level update types delivered by [`crate::Client::next_update`].
2//!
3//! Every update the Telegram server pushes is classified into one of the
4//! variants of [`Update`].  The raw constructor ID and bytes are always
5//! available 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(crate) 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 was sent in.
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, if available (not set for 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    /// Reply to this message with plain text.
73    pub async fn reply(&self, client: &mut Client, text: impl Into<String>) -> Result<(), Error> {
74        let peer = match self.peer_id() {
75            Some(p) => p.clone(),
76            None    => return Err(Error::Deserialize("cannot reply: unknown peer".into())),
77        };
78        client.send_message_to_peer(peer, &text.into()).await
79    }
80}
81
82// ─── MessageDeletion ─────────────────────────────────────────────────────────
83
84/// One or more messages were deleted.
85#[derive(Debug, Clone)]
86pub struct MessageDeletion {
87    /// IDs of the deleted messages.
88    pub message_ids: Vec<i32>,
89    /// Channel ID, if the deletion happened in a channel / supergroup.
90    pub channel_id:  Option<i64>,
91}
92
93// ─── CallbackQuery ───────────────────────────────────────────────────────────
94
95/// A user pressed an inline keyboard button on a bot message.
96#[derive(Debug, Clone)]
97pub struct CallbackQuery {
98    pub query_id:        i64,
99    pub user_id:         i64,
100    pub message_id:      Option<i32>,
101    pub chat_instance:   i64,
102    /// Raw `data` bytes from the button.
103    pub data_raw:        Option<Vec<u8>>,
104    /// Game short name (if a game button was pressed).
105    pub game_short_name: Option<String>,
106}
107
108impl CallbackQuery {
109    /// Button data as a UTF-8 string, if valid.
110    pub fn data(&self) -> Option<&str> {
111        self.data_raw.as_ref().and_then(|d| std::str::from_utf8(d).ok())
112    }
113
114    /// Answer the callback query (removes the loading indicator on the client).
115    pub async fn answer(
116        &self,
117        client: &mut Client,
118        text:   Option<&str>,
119    ) -> Result<(), Error> {
120        client.answer_callback_query(self.query_id, text, false).await.map(|_| ())
121    }
122
123    /// Answer with a popup alert.
124    pub async fn answer_alert(
125        &self,
126        client: &mut Client,
127        text:   &str,
128    ) -> Result<(), Error> {
129        client.answer_callback_query(self.query_id, Some(text), true).await.map(|_| ())
130    }
131}
132
133// ─── InlineQuery ─────────────────────────────────────────────────────────────
134
135/// A user is typing an inline query (`@bot something`).
136#[derive(Debug, Clone)]
137pub struct InlineQuery {
138    pub query_id: i64,
139    pub user_id:  i64,
140    pub query:    String,
141    pub offset:   String,
142}
143
144impl InlineQuery {
145    /// The text the user typed after the bot username.
146    pub fn query(&self) -> &str { &self.query }
147}
148
149// ─── RawUpdate ───────────────────────────────────────────────────────────────
150
151/// A TL update that has no dedicated high-level variant yet.
152#[derive(Debug, Clone)]
153pub struct RawUpdate {
154    /// Constructor ID of the inner update.
155    pub constructor_id: u32,
156}
157
158// ─── Update ───────────────────────────────────────────────────────────────────
159
160/// A high-level event received from Telegram.
161///
162/// See [`crate::Client::next_update`] for usage.
163#[non_exhaustive]
164#[derive(Debug, Clone)]
165pub enum Update {
166    /// A new message (personal chat, group, channel, or bot command).
167    NewMessage(IncomingMessage),
168    /// An existing message was edited.
169    MessageEdited(IncomingMessage),
170    /// One or more messages were deleted.
171    MessageDeleted(MessageDeletion),
172    /// An inline keyboard button was pressed on a bot message.
173    CallbackQuery(CallbackQuery),
174    /// A user typed an inline query for the bot.
175    InlineQuery(InlineQuery),
176    /// A raw TL update not mapped to any of the above variants.
177    Raw(RawUpdate),
178}
179
180// ─── MTProto update container IDs ────────────────────────────────────────────
181
182const ID_UPDATES_TOO_LONG:      u32 = 0xe317af7e;
183const ID_UPDATE_SHORT_MESSAGE:  u32 = 0x313bc7f8;
184const ID_UPDATE_SHORT_CHAT_MSG: u32 = 0x4d6deea5;
185const ID_UPDATE_SHORT:          u32 = 0x78d4dec1;
186const ID_UPDATES:               u32 = 0x74ae4240;
187const ID_UPDATES_COMBINED:      u32 = 0x725b04c3;
188
189// ─── Parser ──────────────────────────────────────────────────────────────────
190
191/// Parse raw update container bytes into high-level [`Update`] values.
192///
193/// Returns an empty vector for unknown or unhandled containers.
194pub(crate) fn parse_updates(bytes: &[u8]) -> Vec<Update> {
195    if bytes.len() < 4 {
196        return vec![];
197    }
198    let cid = u32::from_le_bytes(bytes[..4].try_into().unwrap());
199
200    match cid {
201        ID_UPDATES_TOO_LONG => {
202            log::warn!("updatesTooLong received — some updates may be missed; call getDifference if gap-free delivery is required");
203            vec![]
204        }
205
206        // updateShortMessage — single DM
207        ID_UPDATE_SHORT_MESSAGE => {
208            let mut cur = Cursor::from_slice(bytes);
209            match tl::types::UpdateShortMessage::deserialize(&mut cur) {
210                Ok(m) => {
211                    vec![Update::NewMessage(make_short_dm(m))]
212                }
213                Err(e) => { log::warn!("updateShortMessage parse error: {e}"); vec![] }
214            }
215        }
216
217        // updateShortChatMessage — single group message
218        ID_UPDATE_SHORT_CHAT_MSG => {
219            let mut cur = Cursor::from_slice(bytes);
220            match tl::types::UpdateShortChatMessage::deserialize(&mut cur) {
221                Ok(m) => {
222                    vec![Update::NewMessage(make_short_chat(m))]
223                }
224                Err(e) => { log::warn!("updateShortChatMessage parse error: {e}"); vec![] }
225            }
226        }
227
228        // updateShort — wraps a single Update
229        ID_UPDATE_SHORT => {
230            let mut cur = Cursor::from_slice(bytes);
231            match tl::types::UpdateShort::deserialize(&mut cur) {
232                Ok(u) => {
233                    from_single_update(u.update)
234                }
235                Err(e) => { log::warn!("updateShort parse error: {e}"); vec![] }
236            }
237        }
238
239        // updates / updatesCombined — batch of updates
240        ID_UPDATES => {
241            let mut cur = Cursor::from_slice(bytes);
242            match tl::enums::Updates::deserialize(&mut cur) {
243                Ok(tl::enums::Updates::Updates(u)) => {
244                    u.updates.into_iter().flat_map(from_single_update).collect()
245                }
246                Err(e) => { log::warn!("Updates parse error: {e}"); vec![] }
247                _ => vec![],
248            }
249        }
250
251        ID_UPDATES_COMBINED => {
252            let mut cur = Cursor::from_slice(bytes);
253            match tl::enums::Updates::deserialize(&mut cur) {
254                Ok(tl::enums::Updates::Combined(u)) => {
255                    u.updates.into_iter().flat_map(from_single_update).collect()
256                }
257                Err(e) => { log::warn!("UpdatesCombined parse error: {e}"); vec![] }
258                _ => vec![],
259            }
260        }
261
262        _ => vec![], // Not an updates container (handled elsewhere by dispatch_body)
263    }
264}
265
266/// Convert a single `tl::enums::Update` into a `Vec<Update>` (usually 0 or 1 element).
267fn from_single_update(upd: tl::enums::Update) -> Vec<Update> {
268    use tl::enums::Update::*;
269    match upd {
270        NewMessage(u) => vec![Update::NewMessage(IncomingMessage::from_raw(u.message))],
271        NewChannelMessage(u) => vec![Update::NewMessage(IncomingMessage::from_raw(u.message))],
272        EditMessage(u) => vec![Update::MessageEdited(IncomingMessage::from_raw(u.message))],
273        EditChannelMessage(u) => vec![Update::MessageEdited(IncomingMessage::from_raw(u.message))],
274        DeleteMessages(u) => vec![Update::MessageDeleted(MessageDeletion { message_ids: u.messages, channel_id: None })],
275        DeleteChannelMessages(u) => vec![Update::MessageDeleted(MessageDeletion { message_ids: u.messages, channel_id: Some(u.channel_id) })],
276        BotCallbackQuery(u) => vec![Update::CallbackQuery(CallbackQuery {
277            query_id:        u.query_id,
278            user_id:         u.user_id,
279            message_id:      Some(u.msg_id),
280            chat_instance:   u.chat_instance,
281            data_raw:        u.data,
282            game_short_name: u.game_short_name,
283        })],
284        InlineBotCallbackQuery(u) => vec![Update::CallbackQuery(CallbackQuery {
285            query_id:        u.query_id,
286            user_id:         u.user_id,
287            message_id:      None,
288            chat_instance:   u.chat_instance,
289            data_raw:        u.data,
290            game_short_name: u.game_short_name,
291        })],
292        BotInlineQuery(u) => vec![Update::InlineQuery(InlineQuery {
293            query_id: u.query_id,
294            user_id:  u.user_id,
295            query:    u.query,
296            offset:   u.offset,
297        })],
298        other => {
299            // Use the TL constructor ID as the raw update identifier
300            let cid = tl_constructor_id(&other);
301            vec![Update::Raw(RawUpdate { constructor_id: cid })]
302        }
303    }
304}
305
306/// Extract constructor ID from a `tl::enums::Update` variant.
307fn tl_constructor_id(upd: &tl::enums::Update) -> u32 {
308    use tl::enums::Update::*;
309    match upd {
310        UserStatus(_)               => 0x1bfbd823,
311        ContactsReset               => 0xdeaf4e67,
312        NewEncryptedMessage(_)      => 0x12bcbd9a,
313        EncryptedChatTyping(_)      => 0x1710f156,
314        Encryption(_)               => 0xb4a2e88d,
315        EncryptedMessagesRead(_)    => 0x38fe25b7,
316        ChatParticipants(_)         => 0x07761198,
317        NewMessage(_)               => 0x1f2b0afd,
318        MessageId(_)                => 0x4e90bfd6,
319        ReadMessagesContents(_)     => 0x68c13933,
320        DeleteMessages(_)           => 0xa20db0e5,
321        UserTyping(_)               => 0x5c486927,
322        ChatUserTyping(_)           => 0x9a65ea1f,
323        ChatParticipantAdd(_)       => 0xea4cb65b,
324        ChatParticipantDelete(_)    => 0x6e5f2de1,
325        DcOptions(_)                => 0x8e5e9873,
326        NotifySettings(_)           => 0xbec268ef,
327        ServiceNotification(_)      => 0xebe46819,
328        Privacy(_)                  => 0xee3b272a,
329        UserPhone(_)                => 0x05492a13,
330        ReadHistoryInbox(_)         => 0x9961fd5c,
331        ReadHistoryOutbox(_)        => 0x2f2f21bf,
332        WebPage(_)                  => 0x7f891213,
333        EditMessage(_)              => 0xe40370a3,
334        EditChannelMessage(_)       => 0x1b3f4df7,
335        NewChannelMessage(_)        => 0x62ba04d9,
336        DeleteChannelMessages(_)    => 0xc32d5b12,
337        ChannelMessageViews(_)      => 0x98a12b4b,
338        BotCallbackQuery(_)         => 0xe9ff1938,
339        InlineBotCallbackQuery(_)   => 0x691e9f68,
340        BotInlineQuery(_)           => 0x54826690,
341        BotInlineSend(_)            => 0x0e48f964,
342        _                           => 0x00000000,
343    }
344}
345
346// ─── Short message helpers ────────────────────────────────────────────────────
347
348fn make_short_dm(m: tl::types::UpdateShortMessage) -> IncomingMessage {
349    let msg = tl::types::Message {
350        out:               m.out,
351        mentioned:         m.mentioned,
352        media_unread:      m.media_unread,
353        silent:            m.silent,
354        post:              false,
355        from_scheduled:    false,
356        legacy:            false,
357        edit_hide:         false,
358        pinned:            false,
359        noforwards:        false,
360        invert_media:      false,
361        offline:           false,
362        video_processing_pending: false,
363        id:                m.id,
364        from_id:           Some(tl::enums::Peer::User(tl::types::PeerUser { user_id: m.user_id })),
365        peer_id:           tl::enums::Peer::User(tl::types::PeerUser { user_id: m.user_id }),
366        saved_peer_id:     None,
367        fwd_from:          m.fwd_from,
368        via_bot_id:        m.via_bot_id,
369        via_business_bot_id: None,
370        reply_to:          m.reply_to,
371        date:              m.date,
372        message:           m.message,
373        media:             None,
374        reply_markup:      None,
375        entities:          m.entities,
376        views:             None,
377        forwards:          None,
378        replies:           None,
379        edit_date:         None,
380        post_author:       None,
381        grouped_id:        None,
382        reactions:         None,
383        restriction_reason: None,
384        ttl_period:        None,
385        quick_reply_shortcut_id: None,
386        effect:            None,
387        factcheck:         None,
388        report_delivery_until_date: None,
389        paid_message_stars: None,
390        suggested_post:    None,
391        from_boosts_applied: None,
392        paid_suggested_post_stars: false,
393        paid_suggested_post_ton: false,
394        schedule_repeat_period: None,
395        summary_from_language: None,
396    };
397    IncomingMessage { raw: tl::enums::Message::Message(msg) }
398}
399
400fn make_short_chat(m: tl::types::UpdateShortChatMessage) -> IncomingMessage {
401    let msg = tl::types::Message {
402        out:               m.out,
403        mentioned:         m.mentioned,
404        media_unread:      m.media_unread,
405        silent:            m.silent,
406        post:              false,
407        from_scheduled:    false,
408        legacy:            false,
409        edit_hide:         false,
410        pinned:            false,
411        noforwards:        false,
412        invert_media:      false,
413        offline:           false,
414        video_processing_pending: false,
415        id:                m.id,
416        from_id:           Some(tl::enums::Peer::User(tl::types::PeerUser { user_id: m.from_id })),
417        peer_id:           tl::enums::Peer::Chat(tl::types::PeerChat { chat_id: m.chat_id }),
418        saved_peer_id:     None,
419        fwd_from:          m.fwd_from,
420        via_bot_id:        m.via_bot_id,
421        via_business_bot_id: None,
422        reply_to:          m.reply_to,
423        date:              m.date,
424        message:           m.message,
425        media:             None,
426        reply_markup:      None,
427        entities:          m.entities,
428        views:             None,
429        forwards:          None,
430        replies:           None,
431        edit_date:         None,
432        post_author:       None,
433        grouped_id:        None,
434        reactions:         None,
435        restriction_reason: None,
436        ttl_period:        None,
437        quick_reply_shortcut_id: None,
438        effect:            None,
439        factcheck:         None,
440        report_delivery_until_date: None,
441        paid_message_stars: None,
442        suggested_post:    None,
443        from_boosts_applied: None,
444        paid_suggested_post_stars: false,
445        paid_suggested_post_ton: false,
446        schedule_repeat_period: None,
447        summary_from_language: None,
448    };
449    IncomingMessage { raw: tl::enums::Message::Message(msg) }
450}