Skip to main content

ferogram_msgbox/
lib.rs

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