Skip to main content

ferogram_msgbox/
lib.rs

1/*
2 * Copyright (c) 2026 Ankit Chaubey <ankitchaubey.dev@gmail.com>
3 * https://github.com/ankit-chaubey
4 *
5 * Project: ferogram
6 * Website: https://ferogram.dev
7 *
8 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
9 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
10 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
11 * This file may not be copied, modified, or distributed except according
12 * to those terms.
13 */
14
15#![cfg_attr(docsrs, feature(doc_cfg))]
16#![doc(html_root_url = "https://docs.rs/ferogram-msgbox/0.6.5")]
17//! Update-gap tracking state machine for ferogram: pts/qts/seq bookkeeping,
18//! per-channel state, and gap detection for Telegram's `Updates` stream.
19//!
20//! This crate is part of [ferogram](https://crates.io/crates/ferogram), an async Rust
21//! MTProto client built by [Ankit Chaubey](https://github.com/ankit-chaubey).
22//!
23//! - Channel: [t.me/Ferogram](https://t.me/Ferogram)
24//! - Chat: [t.me/FerogramChat](https://t.me/FerogramChat)
25//!
26//! `ferogram` re-exports this as `ferogram::message_box`, so most people never
27//! depend on it directly. If you're just building a bot or a client, start
28//! with [`ferogram`](https://crates.io/crates/ferogram) instead.
29//!
30//! # What it does
31//!
32//! Telegram's `Updates` stream is lossy over the wire, pushed updates can
33//! drop or arrive out of order, but the protocol carries a `pts`/`qts`/`seq`
34//! counter in every update, so a client can detect exactly when it missed
35//! something and ask for a diff to fill the hole. [`MessageBoxes`] is the
36//! state machine that does that bookkeeping:
37//!
38//! - Tracks the global `pts`/`qts`/`seq`/`date` counters, plus a separate
39//!   `pts` per channel.
40//! - Detects gaps (an update arrives with a `pts_count` that doesn't chain
41//!   onto the last known `pts`) and buffers out-of-order updates for a short
42//!   window before giving up and requesting a diff.
43//! - Decides when a periodic catch-up `getDifference` is due, even with no
44//!   known gap (`NO_UPDATES_TIMEOUT`, 15 minutes).
45//! - Classifies your own outgoing RPC responses ([`classify_own_response`])
46//!   so updates piggybacked on them, e.g. `sendMessage` returning the new
47//!   message, get folded into the same pts sequence.
48//! - Applies `updates.getDifference` / `updates.getChannelDifference`
49//!   results back into state once the caller has fetched them.
50//!
51//! It's a pure state machine: no async, no networking, no RPC calls. It
52//! takes an [`UpdatesLike`] in via [`MessageBoxes::process_updates`] and
53//! hands back either the update batch (plus any referenced users/chats) or a
54//! [`Gap`], which tells the caller which RPC to run and how to feed the
55//! result back.
56//!
57//! # Example
58//!
59//! ```rust,ignore
60//! use ferogram_msgbox::{MessageBoxes, UpdatesLike};
61//!
62//! let mut mbox = MessageBoxes::new();
63//!
64//! // Feed a pushed update frame (or an own-RPC response) in.
65//! match mbox.process_updates(UpdatesLike::Updates(Box::new(incoming))) {
66//!     Ok((updates, users, chats)) => {
67//!         // dispatch each Vec's items as usual.
68//!     }
69//!     Err(_gap) => {
70//!         // A gap was detected (or a diff was already due). Fetch it and
71//!         // feed the result back:
72//!         if let Some(req) = mbox.get_difference() {
73//!             let diff = client.invoke(&req).await?; // tl::enums::updates::Difference
74//!             let (updates, users, chats) = mbox.apply_difference(diff);
75//!             // dispatch updates/users/chats
76//!         }
77//!     }
78//! }
79//!
80//! // Called periodically (e.g. every tick of your update loop) even with no
81//! // known gap, handles the 15-minute no-updates safety net and any pending
82//! // per-channel diffs.
83//! let deadline = mbox.check_deadlines();
84//! ```
85//!
86//! On reconnect, feed [`UpdatesLike::ConnectionClosed`] in, anything sent
87//! while the socket was down (or before you were listening) is exactly the
88//! kind of gap this is designed to catch.
89
90#![deny(unsafe_code)]
91
92// OVERVIEW
93// This module implements the SINGLE UPDATE AUTHORITY for ferogram.
94// Previously, ferogram scattered update-gap handling across:
95//   - PtsState + PossibleGapBuffer (pts.rs)
96//   - check_and_fill_gap / check_and_fill_channel_gap / check_and_fill_qts_gap (pts.rs)
97//   - check_update_deadline (pts.rs)
98//   - Hard-coded getDifference calls inside dispatch_updates (lib.rs)
99// This caused overlapping recovery paths, races between concurrent diff calls,
100// and PFS bind operations interfering with gap recovery.
101//   - MessageBoxes is a PURE STATE MACHINE (no async, no RPC calls).
102//   - ONE entry point: process_updates().
103//   - The caller checks check_deadlines() to know when to force getDifference.
104//   - The caller executes the RPC (get_difference / get_channel_difference)
105//     and feeds the result back via apply_difference / apply_channel_difference.
106//   - Gap buffering lives inside each LiveEntry (not in a separate global buffer).
107//   - PFS / reconnect only interacts via UpdatesLike::ConnectionClosed.
108
109mod adaptor;
110pub mod defs;
111
112#[cfg(test)]
113mod tests;
114
115use std::cmp::Ordering;
116use std::time::Duration;
117
118use defs::Instant; // real std::time::Instant under cfg(not(test)), fake_clock::Instant under cfg(test)
119
120use defs::Key;
121pub use defs::{ChannelState, Gap, MessageBoxes, UpdatesLike, UpdatesStateSnap};
122use defs::{
123    LiveEntry, NO_DATE, NO_PTS, NO_SEQ, POSSIBLE_GAP_TIMEOUT, PossibleGap, PtsInfo, UpdateAndPeers,
124};
125use ferogram_tl_types as tl;
126
127// Own-response classification
128
129/// Parse the raw body of one of our own RPC responses into an [`UpdatesLike`],
130/// so self-sent actions (sending/editing/deleting a message, etc.) can be fed
131/// back through [`MessageBoxes::process_updates`] the same way a pushed update
132/// would be. Otherwise the next real update looks like a gap and triggers a
133/// spurious getDifference.
134///
135/// This is pure parsing - no `Client` or lock access. The caller is
136/// responsible for locking its `MessageBoxes` and calling `process_updates()`
137/// with the result.
138///
139/// Safe to call on any response: every TL type is prefixed with a 4-byte
140/// constructor ID, and `Updates::deserialize` checks it before matching a
141/// variant. A response that's neither `Updates`, a joined-chat invite result,
142/// nor `messages.AffectedMessages` just fails to match and this returns `None`.
143pub fn classify_own_response(body: &[u8]) -> Option<UpdatesLike> {
144    use tl::{Deserializable, Identifiable};
145
146    if body.len() < 4 {
147        return None;
148    }
149
150    // Most write RPCs return `Updates` (sendMessage, editMessage, forwardMessages, leaveChannel, ...).
151    let mut cur = tl::Cursor::from_slice(body);
152    if let Ok(updates) = tl::enums::Updates::deserialize(&mut cur) {
153        return Some(UpdatesLike::Updates(Box::new(updates)));
154    }
155
156    // importChatInvite/joinChannel return ChatInviteJoinResult now, not a
157    // bare Updates, so the check above misses them. Ok still wraps a
158    // real Updates (pts/seq) - unwrap and feed it. WebView has no
159    // updates to feed.
160    let mut cur = tl::Cursor::from_slice(body);
161    if let Ok(tl::enums::messages::ChatInviteJoinResult::Ok(ok)) =
162        tl::enums::messages::ChatInviteJoinResult::deserialize(&mut cur)
163    {
164        return Some(UpdatesLike::Updates(Box::new(ok.updates)));
165    }
166
167    // A few (deleteMessages, readHistory, ...) return bare messages.AffectedMessages
168    // instead. It's not an enum, so deserialize() won't check the ctor id for us.
169    let id = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
170    if id == <tl::types::messages::AffectedMessages as Identifiable>::CONSTRUCTOR_ID {
171        let mut cur = tl::Cursor::from_slice(&body[4..]);
172        if let Ok(affected) = tl::types::messages::AffectedMessages::deserialize(&mut cur) {
173            return Some(UpdatesLike::AffectedMessages(affected));
174        }
175    }
176
177    None
178}
179
180// Helpers
181
182fn next_updates_deadline() -> Instant {
183    Instant::now() + defs::NO_UPDATES_TIMEOUT
184}
185
186fn update_sort_key(update: &tl::enums::Update) -> i32 {
187    match PtsInfo::from_update(update) {
188        Some(info) => info.pts - info.count,
189        None => NO_PTS,
190    }
191}
192
193// Creation and state management
194
195impl Default for MessageBoxes {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl MessageBoxes {
202    /// Create a new, empty [`MessageBoxes`] (no prior state).
203    pub fn new() -> Self {
204        tracing::trace!("[ferogram::msgbox] created new (no prior state)");
205        Self {
206            entries: Vec::new(),
207            date: NO_DATE,
208            seq: NO_SEQ,
209            getting_diff_for: Vec::new(),
210            channel_diff_in_flight: None,
211            next_deadline: next_updates_deadline(),
212        }
213    }
214
215    /// Create a [`MessageBoxes`] from a previously-persisted snapshot.
216    pub fn load(state: UpdatesStateSnap) -> Self {
217        tracing::trace!("[ferogram::msgbox] loaded from state: {:?}", state);
218        let mut entries = Vec::with_capacity(2 + state.channels.len());
219        let mut getting_diff_for = Vec::with_capacity(2 + state.channels.len());
220        let deadline = next_updates_deadline();
221
222        if state.pts != NO_PTS {
223            entries.push(LiveEntry {
224                key: Key::Common,
225                pts: state.pts,
226                deadline,
227                possible_gap: None,
228            });
229        }
230        if state.qts != NO_PTS {
231            entries.push(LiveEntry {
232                key: Key::Secondary,
233                pts: state.qts,
234                deadline,
235                possible_gap: None,
236            });
237        }
238        entries.extend(state.channels.iter().map(|c| LiveEntry {
239            key: Key::Channel(c.id),
240            pts: c.pts,
241            deadline,
242            possible_gap: None,
243        }));
244        entries.sort_by_key(|e| e.key);
245
246        // On load we need to reconcile; mark all entries as needing diff.
247        getting_diff_for.extend(entries.iter().map(|e| e.key));
248
249        Self {
250            entries,
251            date: state.date,
252            seq: state.seq,
253            getting_diff_for,
254            channel_diff_in_flight: None,
255            next_deadline: deadline,
256        }
257    }
258
259    fn entry(&self, key: Key) -> Option<&LiveEntry> {
260        self.entries
261            .binary_search_by_key(&key, |e| e.key)
262            .map(|i| &self.entries[i])
263            .ok()
264    }
265
266    fn update_entry(&mut self, key: Key, f: impl FnOnce(&mut LiveEntry)) -> bool {
267        match self.entries.binary_search_by_key(&key, |e| e.key) {
268            Ok(i) => {
269                f(&mut self.entries[i]);
270                true
271            }
272            Err(_) => false,
273        }
274    }
275
276    fn force_update_entry(&mut self, mut entry: LiveEntry, f: impl FnOnce(&mut LiveEntry)) {
277        match self.entries.binary_search_by_key(&entry.key, |e| e.key) {
278            Ok(i) => f(&mut self.entries[i]),
279            Err(i) => {
280                f(&mut entry);
281                self.entries.insert(i, entry);
282            }
283        }
284    }
285
286    fn set_entry(&mut self, entry: LiveEntry) {
287        match self.entries.binary_search_by_key(&entry.key, |e| e.key) {
288            Ok(i) => self.entries[i] = entry,
289            Err(i) => self.entries.insert(i, entry),
290        }
291    }
292
293    fn set_pts(&mut self, key: Key, pts: i32) {
294        if !self.update_entry(key, |e| e.pts = pts) {
295            self.set_entry(LiveEntry {
296                key,
297                pts,
298                deadline: next_updates_deadline(),
299                possible_gap: None,
300            });
301        }
302    }
303
304    fn pop_entry(&mut self, key: Key) -> Option<LiveEntry> {
305        match self.entries.binary_search_by_key(&key, |e| e.key) {
306            Ok(i) => Some(self.entries.remove(i)),
307            Err(_) => None,
308        }
309    }
310
311    fn reset_deadline(&mut self, key: Key, deadline: Instant) {
312        let mut old_deadline = self.next_deadline;
313        self.update_entry(key, |e| {
314            old_deadline = e.deadline;
315            e.deadline = deadline;
316        });
317        if self.next_deadline == old_deadline {
318            self.next_deadline = self
319                .entries
320                .iter()
321                .fold(deadline, |d, e| d.min(e.effective_deadline()));
322        }
323    }
324
325    fn reset_timeout(&mut self, key: Key, timeout: Option<i32>) {
326        self.reset_deadline(
327            key,
328            timeout
329                .map(|t| Instant::now() + Duration::from_secs(t as _))
330                .unwrap_or_else(next_updates_deadline),
331        );
332    }
333
334    fn try_begin_get_diff(&mut self, key: Key) {
335        if !self.getting_diff_for.contains(&key) {
336            if self.update_entry(key, |e| e.possible_gap = None) {
337                self.getting_diff_for.push(key);
338            } else {
339                tracing::debug!(
340                    "[ferogram::msgbox] begin_get_diff skipped for {:?}: no entry exists",
341                    key
342                );
343            }
344        }
345    }
346
347    fn try_end_get_diff(&mut self, key: Key) {
348        let i = match self.getting_diff_for.iter().position(|&k| k == key) {
349            Some(i) => i,
350            None => return,
351        };
352        self.getting_diff_for.remove(i);
353        self.reset_deadline(key, next_updates_deadline());
354        debug_assert!(
355            self.entry(key).is_none_or(|e| e.possible_gap.is_none()),
356            "gaps shouldn't be created while getting difference"
357        );
358    }
359
360    /// Whether this box has any state yet.
361    pub fn is_empty(&self) -> bool {
362        self.entries.is_empty()
363    }
364
365    /// Set state right after login (must only call when [`Self::is_empty`] is true).
366    pub fn set_state(&mut self, state: tl::types::updates::State) {
367        debug_assert!(self.is_empty());
368        let deadline = next_updates_deadline();
369        self.set_entry(LiveEntry {
370            key: Key::Common,
371            pts: state.pts,
372            deadline,
373            possible_gap: None,
374        });
375        self.set_entry(LiveEntry {
376            key: Key::Secondary,
377            pts: state.qts,
378            deadline,
379            possible_gap: None,
380        });
381        self.date = state.date;
382        self.seq = state.seq;
383        self.next_deadline = deadline;
384    }
385
386    /// Record channel pts from `GetDialogs` - does nothing if the channel is already tracked.
387    pub fn try_set_channel_state(&mut self, id: i64, pts: i32) {
388        if self.entry(Key::Channel(id)).is_none() {
389            self.set_entry(LiveEntry {
390                key: Key::Channel(id),
391                pts,
392                deadline: next_updates_deadline(),
393                possible_gap: None,
394            });
395        }
396    }
397
398    /// Current snapshot suitable for session persistence.
399    pub fn session_state(&self) -> UpdatesStateSnap {
400        UpdatesStateSnap {
401            pts: self.entry(Key::Common).map(|e| e.pts).unwrap_or(NO_PTS),
402            qts: self.entry(Key::Secondary).map(|e| e.pts).unwrap_or(NO_PTS),
403            date: self.date,
404            seq: self.seq,
405            channels: self
406                .entries
407                .iter()
408                .filter_map(|e| match e.key {
409                    Key::Channel(id) => Some(ChannelState { id, pts: e.pts }),
410                    _ => None,
411                })
412                .collect(),
413        }
414    }
415
416    /// Return the next deadline instant.
417    ///
418    /// Call this in the select! loop to know when to wake up and check for
419    /// expired gaps or timeouts.  When entries need diff, returns `now` so the
420    /// caller immediately runs the diff.
421    pub fn check_deadlines(&mut self) -> Instant {
422        let now = Instant::now();
423
424        if !self.getting_diff_for.is_empty() {
425            return now; // fire the deadline arm immediately when diff is pending
426        }
427
428        if now >= self.next_deadline {
429            // Mark entries whose deadlines have elapsed.
430            self.getting_diff_for
431                .extend(self.entries.iter().filter_map(|e| {
432                    if now >= e.effective_deadline() {
433                        tracing::debug!(
434                            "[ferogram::msgbox] deadline met for {:?}; forcing diff",
435                            e.key
436                        );
437                        Some(e.key)
438                    } else {
439                        None
440                    }
441                }));
442
443            // Clear possible-gaps for entries we're about to diff.
444            for i in 0..self.getting_diff_for.len() {
445                self.update_entry(self.getting_diff_for[i], |e| e.possible_gap = None);
446            }
447
448            if self.getting_diff_for.is_empty() {
449                self.next_deadline = next_updates_deadline();
450            }
451        }
452
453        self.next_deadline
454    }
455
456    /// Return the `GetDifference` request to execute, if any.
457    ///
458    /// The caller is responsible for executing the RPC and calling
459    /// [`Self::apply_difference`] with the result.
460    pub fn get_difference(&self) -> Option<tl::functions::updates::GetDifference> {
461        for key in [Key::Common, Key::Secondary] {
462            if self.getting_diff_for.contains(&key) {
463                let pts = self
464                    .entry(Key::Common)
465                    .map(|e| e.pts)
466                    .expect("Common entry must exist when diffing it");
467
468                return Some(tl::functions::updates::GetDifference {
469                    pts,
470                    pts_limit: None,
471                    pts_total_limit: None,
472                    date: self.date.max(1),
473                    qts: self.entry(Key::Secondary).map(|e| e.pts).unwrap_or(NO_PTS),
474                    qts_limit: None,
475                });
476            }
477        }
478        None
479    }
480
481    /// Return the skeleton of a `GetChannelDifference` request, if any channel needs it.
482    ///
483    /// The caller must fill in `access_hash` and `limit` before executing.
484    /// Call [`Self::apply_channel_difference`] or [`Self::end_channel_difference`] with the result.
485    pub fn get_channel_difference(
486        &mut self,
487    ) -> Option<(i64, tl::functions::updates::GetChannelDifference)> {
488        // Skip any channel whose request is already in flight (response not
489        // yet applied) - prevents issuing a second concurrent
490        // getChannelDifference for the same channel, e.g. across a
491        // reconnect-generation boundary.
492        let (key, channel_id) = self.getting_diff_for.iter().find_map(|&k| match k {
493            Key::Channel(id) if self.channel_diff_in_flight != Some(id) => Some((k, id)),
494            _ => None,
495        })?;
496
497        let pts = self
498            .entry(key)
499            .map(|e| e.pts)
500            .expect("Channel entry must exist when diffing it");
501
502        self.channel_diff_in_flight = Some(channel_id);
503
504        Some((
505            channel_id,
506            tl::functions::updates::GetChannelDifference {
507                force: false,
508                channel: tl::enums::InputChannel::InputChannel(tl::types::InputChannel {
509                    channel_id,
510                    access_hash: 0, // caller must fill this in
511                }),
512                filter: tl::enums::ChannelMessagesFilter::Empty,
513                pts,
514                limit: 0, // caller must fill this in
515            },
516        ))
517    }
518}
519
520// Normal update flow
521
522impl MessageBoxes {
523    /// Process an update batch.  Returns `Ok((updates, users, chats))` or
524    /// `Err(Gap)` when a gap has been detected and `get_difference` must be called.
525    ///
526    /// This is the **single entry point** for all incoming updates.
527    pub fn process_updates(&mut self, updates: UpdatesLike) -> Result<UpdateAndPeers, Gap> {
528        let deadline = next_updates_deadline();
529
530        let tl::types::UpdatesCombined {
531            date,
532            seq_start,
533            seq,
534            mut updates,
535            users,
536            chats,
537        } = match adaptor::adapt(updates) {
538            Ok(combined) => combined,
539            Err(Gap) => {
540                self.try_begin_get_diff(Key::Common);
541                return Err(Gap);
542            }
543        };
544
545        let new_date = if date == NO_DATE { self.date } else { date };
546        let new_seq = if seq == NO_SEQ { self.seq } else { seq };
547
548        // Check seq for Updates / UpdatesCombined containers.
549        if seq_start != NO_SEQ {
550            match (self.seq + 1).cmp(&seq_start) {
551                Ordering::Equal => {} // apply
552                Ordering::Greater => {
553                    tracing::debug!(
554                        "[ferogram::msgbox] duplicate seq (local={}, remote={}), skipping",
555                        self.seq,
556                        seq_start
557                    );
558                    return Ok((Vec::new(), users, chats));
559                }
560                Ordering::Less => {
561                    tracing::debug!(
562                        "[ferogram::msgbox] seq gap (local={}, remote={})",
563                        self.seq,
564                        seq_start
565                    );
566                    self.try_begin_get_diff(Key::Common);
567                    return Err(Gap);
568                }
569            }
570        }
571
572        // Sort so out-of-order updates within a container are applied in pts order.
573        updates.sort_by_key(update_sort_key);
574
575        let mut result: Vec<tl::enums::Update> = Vec::with_capacity(updates.len());
576        let mut have_unresolved_gaps = false;
577
578        for update in updates {
579            // ChannelTooLong is handled specially.
580            if let tl::enums::Update::ChannelTooLong(ref u) = update {
581                let key = Key::Channel(u.channel_id);
582                if let Some(pts) = u.pts {
583                    self.set_entry(LiveEntry {
584                        key,
585                        pts,
586                        deadline,
587                        possible_gap: None,
588                    });
589                }
590                self.try_begin_get_diff(key);
591                continue;
592            }
593
594            let info = match PtsInfo::from_update(&update) {
595                Some(info) => info,
596                None => {
597                    // No pts: can be applied in any order.
598                    result.push(update);
599                    continue;
600                }
601            };
602
603            // While getting diff for this key, ignore matching updates (they
604            // will arrive again via apply_difference / apply_channel_difference).
605            if self.getting_diff_for.contains(&info.key) {
606                tracing::debug!(
607                    "[ferogram::msgbox] update for {:?} suppressed while getDifference is in flight",
608                    info.key
609                );
610                self.reset_deadline(info.key, next_updates_deadline());
611                result.push(update);
612                continue;
613            }
614
615            let mut gap_deadline = None;
616
617            self.force_update_entry(
618                LiveEntry {
619                    key: info.key,
620                    pts: info.pts - info.count,
621                    deadline,
622                    possible_gap: None,
623                },
624                |entry| {
625                    match (entry.pts + info.count).cmp(&info.pts) {
626                        Ordering::Equal => {
627                            // Normal in-order update.
628                            entry.pts = info.pts;
629                            entry.deadline = deadline;
630                            result.push(update);
631                        }
632                        Ordering::Greater => {
633                            // Duplicate.
634                            tracing::debug!(
635                                "[ferogram::msgbox] duplicate update for {:?} \
636                                 (local={}, count={}, remote={})",
637                                info.key,
638                                entry.pts,
639                                info.count,
640                                info.pts
641                            );
642                        }
643                        Ordering::Less => {
644                            // Gap: buffer and wait.
645                            tracing::debug!(
646                                "[ferogram::msgbox] gap for {:?} \
647                                 (local={}, count={}, remote={})",
648                                info.key,
649                                entry.pts,
650                                info.count,
651                                info.pts
652                            );
653                            entry
654                                .possible_gap
655                                .get_or_insert_with(|| PossibleGap {
656                                    deadline: Instant::now() + POSSIBLE_GAP_TIMEOUT,
657                                    updates: Vec::new(),
658                                })
659                                .updates
660                                .push(update.clone());
661                        }
662                    }
663
664                    // Try to drain the possible_gap buffer now that pts advanced.
665                    if let Some(mut gap) = entry.possible_gap.take() {
666                        gap.updates.sort_by_key(|u| -update_sort_key(u));
667                        while let Some(gap_update) = gap.updates.pop() {
668                            let gap_info = PtsInfo::from_update(&gap_update)
669                                .expect("only updates with pts may be buffered as gaps");
670                            match (entry.pts + gap_info.count).cmp(&gap_info.pts) {
671                                Ordering::Equal => {
672                                    entry.pts = gap_info.pts;
673                                    result.push(gap_update);
674                                }
675                                Ordering::Greater => {}
676                                Ordering::Less => {
677                                    gap.updates.push(gap_update);
678                                    break;
679                                }
680                            }
681                        }
682                        if !gap.updates.is_empty() {
683                            gap_deadline = Some(gap.deadline);
684                            entry.possible_gap = Some(gap);
685                            have_unresolved_gaps = true;
686                        }
687                    }
688                },
689            );
690
691            self.next_deadline = self.next_deadline.min(gap_deadline.unwrap_or(deadline));
692        }
693
694        if !result.is_empty() && !have_unresolved_gaps {
695            self.date = new_date;
696            self.seq = new_seq;
697        }
698
699        Ok((result, users, chats))
700    }
701}
702
703// Applying getDifference results
704
705impl MessageBoxes {
706    /// Apply the result of a `GetDifference` RPC call.
707    pub fn apply_difference(
708        &mut self,
709        difference: tl::enums::updates::Difference,
710    ) -> UpdateAndPeers {
711        tracing::trace!("[ferogram::msgbox] applying account difference");
712        if !self.getting_diff_for.contains(&Key::Common)
713            && !self.getting_diff_for.contains(&Key::Secondary)
714        {
715            tracing::warn!(
716                "[ferogram::msgbox] apply_difference called but no diff was pending \
717                 (concurrent call already completed?); ignoring"
718            );
719            return (Vec::new(), Vec::new(), Vec::new());
720        }
721
722        let finish: bool;
723        let result = match difference {
724            tl::enums::updates::Difference::Empty(e) => {
725                tracing::debug!(
726                    "[ferogram::msgbox] difference empty (date={}, seq={})",
727                    e.date,
728                    e.seq
729                );
730                finish = true;
731                self.date = e.date;
732                self.seq = e.seq;
733                (Vec::new(), Vec::new(), Vec::new())
734            }
735            tl::enums::updates::Difference::Difference(d) => {
736                tracing::debug!(
737                    "[ferogram::msgbox] getDifference: received full difference, applying"
738                );
739                finish = true;
740                self.apply_difference_type(d)
741            }
742            tl::enums::updates::Difference::Slice(tl::types::updates::DifferenceSlice {
743                new_messages,
744                new_encrypted_messages,
745                other_updates,
746                chats,
747                users,
748                intermediate_state: state,
749            }) => {
750                tracing::debug!(
751                    "[ferogram::msgbox] getDifference: received slice, will request another round"
752                );
753                finish = false;
754                self.apply_difference_type(tl::types::updates::Difference {
755                    new_messages,
756                    new_encrypted_messages,
757                    other_updates,
758                    chats,
759                    users,
760                    state,
761                })
762            }
763            tl::enums::updates::Difference::TooLong(d) => {
764                tracing::warn!(
765                    "[ferogram::msgbox] getDifference returned TooLong (pts={}); resetting to server pts",
766                    d.pts
767                );
768                finish = true;
769                self.set_pts(Key::Common, d.pts);
770                (Vec::new(), Vec::new(), Vec::new())
771            }
772        };
773
774        if finish {
775            self.try_end_get_diff(Key::Common);
776            self.try_end_get_diff(Key::Secondary);
777        }
778
779        result
780    }
781
782    fn apply_difference_type(
783        &mut self,
784        tl::types::updates::Difference {
785            new_messages,
786            new_encrypted_messages,
787            other_updates: updates,
788            chats,
789            users,
790            state: tl::enums::updates::State::State(state),
791        }: tl::types::updates::Difference,
792    ) -> UpdateAndPeers {
793        self.date = state.date;
794        self.seq = state.seq;
795        self.set_pts(Key::Common, state.pts);
796        self.set_pts(Key::Secondary, state.qts);
797
798        // Process other_updates through the normal path (handles ChannelTooLong etc).
799        let us = UpdatesLike::Updates(Box::new(tl::enums::Updates::Updates(tl::types::Updates {
800            updates,
801            users,
802            chats,
803            date: NO_DATE,
804            seq: NO_SEQ,
805        })));
806        let (mut result_updates, users, chats) = self
807            .process_updates(us)
808            .expect("gap detected while applying difference - should not happen");
809
810        // Prepend new_messages as UpdateNewMessage with NO_PTS so they bypass pts checks.
811        let msgs: Vec<tl::enums::Update> = new_messages
812            .into_iter()
813            .map(|msg| {
814                tl::enums::Update::NewMessage(tl::types::UpdateNewMessage {
815                    message: msg,
816                    pts: NO_PTS,
817                    pts_count: 0,
818                })
819            })
820            .chain(new_encrypted_messages.into_iter().map(|msg| {
821                tl::enums::Update::NewEncryptedMessage(tl::types::UpdateNewEncryptedMessage {
822                    message: msg,
823                    qts: NO_PTS,
824                })
825            }))
826            .collect();
827
828        result_updates.splice(0..0, msgs);
829        (result_updates, users, chats)
830    }
831}
832
833// Applying getChannelDifference results
834
835impl MessageBoxes {
836    /// Apply the result of a `GetChannelDifference` RPC call.
837    pub fn apply_channel_difference(
838        &mut self,
839        difference: tl::enums::updates::ChannelDifference,
840    ) -> UpdateAndPeers {
841        let Some(channel_id) = self.channel_diff_in_flight.take() else {
842            tracing::warn!(
843                "[ferogram::msgbox] apply_channel_difference called but no channel diff \
844                 was in flight (stale/duplicate response?); ignoring"
845            );
846            return (Vec::new(), Vec::new(), Vec::new());
847        };
848
849        let key = Key::Channel(channel_id);
850        if !self.getting_diff_for.contains(&key) {
851            // The entry was already finalized by an earlier (duplicate)
852            // response for this same channel; this response is stale.
853            tracing::debug!(
854                "[ferogram::msgbox] apply_channel_difference: channel {} no longer in \
855                 getting_diff_for (stale duplicate response); ignoring",
856                channel_id
857            );
858            return (Vec::new(), Vec::new(), Vec::new());
859        }
860
861        tracing::trace!(
862            "[ferogram::msgbox] applying channel {} difference",
863            channel_id
864        );
865        self.update_entry(key, |e| e.possible_gap = None);
866
867        let Ok(tl::types::updates::ChannelDifference {
868            r#final,
869            pts,
870            timeout,
871            new_messages,
872            other_updates: updates,
873            chats,
874            users,
875        }) = adaptor::adapt_channel_difference(difference)
876        else {
877            // Malformed-but-legitimate shape (no pts to advance to); give up on
878            // this response and let the channel retry through the normal path
879            // instead of applying garbage state or crashing.
880            self.channel_diff_in_flight = Some(channel_id);
881            self.end_channel_difference(PrematureEndReason::TemporaryServerIssues);
882            return (Vec::new(), Vec::new(), Vec::new());
883        };
884
885        if r#final {
886            tracing::debug!(
887                "[ferogram::msgbox] channel {} diff complete (final=true)",
888                channel_id
889            );
890            self.try_end_get_diff(key);
891        } else {
892            tracing::debug!(
893                "[ferogram::msgbox] channel {} diff slice received; requesting next batch",
894                channel_id
895            );
896        }
897
898        self.set_pts(key, pts);
899
900        let us = UpdatesLike::Updates(Box::new(tl::enums::Updates::Updates(tl::types::Updates {
901            updates,
902            users,
903            chats,
904            date: NO_DATE,
905            seq: NO_SEQ,
906        })));
907        let (mut result_updates, users, chats) = self
908            .process_updates(us)
909            .expect("gap detected while applying channel difference");
910
911        // Prepend new_messages.
912        let msgs: Vec<tl::enums::Update> = new_messages
913            .into_iter()
914            .map(|msg| {
915                tl::enums::Update::NewChannelMessage(tl::types::UpdateNewChannelMessage {
916                    message: msg,
917                    pts: NO_PTS,
918                    pts_count: 0,
919                })
920            })
921            .collect();
922        result_updates.splice(0..0, msgs);
923
924        self.reset_timeout(key, timeout);
925
926        (result_updates, users, chats)
927    }
928
929    /// Mark a channel diff as ended prematurely (access error or ban).
930    /// Abort a pending global difference after a parse or RPC failure.
931    ///
932    /// Clears `Key::Common` and `Key::Secondary` from `getting_diff_for` so
933    /// that `check_deadlines()` stops returning `Instant::now()` and the
934    /// deadline arm stops spawning hundreds of no-op tasks while the previous
935    /// attempt's backoff sleep is still running.
936    ///
937    /// The pts counters are left unchanged; the next real update gap will
938    /// re-queue getDifference automatically.
939    pub fn abort_difference(&mut self) {
940        for key in [Key::Common, Key::Secondary] {
941            self.update_entry(key, |e| e.possible_gap = None);
942            self.try_end_get_diff(key);
943        }
944        tracing::debug!(
945            "[ferogram::msgbox] getDifference aborted; cleared pending state for Common and Secondary"
946        );
947    }
948
949    /// Forcibly advance Common + Secondary pts to the server-reported values.
950    ///
951    /// Called after a getDifference parse failure (unknown TL constructor from a
952    /// newer Telegram layer) so the stale pts gap does not re-trigger getDifference
953    /// in an infinite loop.  Channel entries are left unchanged.
954    pub fn force_reset_common_pts(&mut self, pts: i32, qts: i32, date: i32, seq: i32) {
955        self.set_pts(Key::Common, pts);
956        self.set_pts(Key::Secondary, qts);
957        self.date = date;
958        self.seq = seq;
959        tracing::debug!(
960            "[ferogram::msgbox] force_reset_common_pts: pts={pts}, qts={qts}, seq={seq}"
961        );
962    }
963
964    pub fn end_channel_difference(&mut self, reason: PrematureEndReason) {
965        let Some(channel_id) = self.channel_diff_in_flight.take() else {
966            tracing::warn!(
967                "[ferogram::msgbox] end_channel_difference called but no channel diff \
968                 was in flight (already ended? duplicate error path)"
969            );
970            return;
971        };
972        let key = Key::Channel(channel_id);
973        if !self.getting_diff_for.contains(&key) {
974            tracing::debug!(
975                "[ferogram::msgbox] end_channel_difference: channel {} no longer in \
976                 getting_diff_for (stale duplicate response); ignoring",
977                channel_id
978            );
979            return;
980        }
981
982        tracing::trace!(
983            "[ferogram::msgbox] ending channel {} diff: {:?}",
984            channel_id,
985            reason
986        );
987
988        match reason {
989            PrematureEndReason::TemporaryServerIssues => {
990                self.update_entry(key, |e| e.possible_gap = None);
991                self.try_end_get_diff(key);
992            }
993            PrematureEndReason::Banned => {
994                self.update_entry(key, |e| e.possible_gap = None);
995                self.try_end_get_diff(key);
996                self.pop_entry(key);
997            }
998        }
999    }
1000}
1001
1002/// Reason for calling [`MessageBoxes::end_channel_difference`].
1003#[derive(Debug)]
1004pub enum PrematureEndReason {
1005    /// Temporary failure; keep the entry and retry later.
1006    TemporaryServerIssues,
1007    /// The account has been banned; remove the entry permanently.
1008    Banned,
1009}