Skip to main content

layer_client/
dc_migration.rs

1// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4// NOTE:
5// The "Layer" project is no longer maintained or supported.
6// Its original purpose for personal SDK/APK experimentation and learning
7// has been fulfilled.
8//
9// Please use Ferogram instead:
10// https://github.com/ankit-chaubey/ferogram
11// Ferogram will receive future updates and development, although progress
12// may be slower.
13//
14// Ferogram is an async Telegram MTProto client library written in Rust.
15// Its implementation follows the behaviour of the official Telegram clients,
16// particularly Telegram Desktop and TDLib, and aims to provide a clean and
17// modern async interface for building Telegram clients and tools.
18
19//! DC migration helpers.
20
21use layer_tl_types as tl;
22use std::sync::Mutex;
23
24use crate::errors::InvocationError;
25
26// Static DC address table.
27// Exposes a pub fn so migrate_to and tests can reference it without hardcoding strings.
28
29/// Return the statically known IPv4 address for a Telegram DC.
30///
31/// Used as a fallback when the DC is not yet in the session's dc_options table
32/// (i.e. first migration to a DC we haven't talked to before).
33///
34/// Source: https://core.telegram.org/mtproto/DC
35pub fn fallback_dc_addr(dc_id: i32) -> &'static str {
36    match dc_id {
37        1 => "149.154.175.53:443",
38        2 => "149.154.167.51:443",
39        3 => "149.154.175.100:443",
40        4 => "149.154.167.91:443",
41        5 => "91.108.56.130:443",
42        _ => "149.154.167.51:443", // DC2 as last resort
43    }
44}
45
46/// Build the initial DC options map from the static table.
47pub fn default_dc_addresses() -> Vec<(i32, String)> {
48    (1..=5)
49        .map(|id| (id, fallback_dc_addr(id).to_string()))
50        .collect()
51}
52
53// When operating on a non-home DC (e.g. downloading from DC4 while home is DC1),
54// the client must export its auth from home and import it on the target DC.
55// We track which DCs already have a copy to avoid redundant round-trips.
56
57/// State that must live inside `ClientInner` to track which DCs already have
58/// a copy of the account's authorization key.
59pub struct DcAuthTracker {
60    copied: Mutex<Vec<i32>>,
61}
62
63impl DcAuthTracker {
64    pub fn new() -> Self {
65        Self {
66            copied: Mutex::new(Vec::new()),
67        }
68    }
69
70    /// Check if we have already copied auth to `dc_id`.
71    pub fn has_copied(&self, dc_id: i32) -> bool {
72        self.copied.lock().unwrap().contains(&dc_id)
73    }
74
75    /// Mark `dc_id` as having received a copy of the auth.
76    pub fn mark_copied(&self, dc_id: i32) {
77        self.copied.lock().unwrap().push(dc_id);
78    }
79}
80
81impl Default for DcAuthTracker {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87/// Export the home-DC authorization and import it on `target_dc_id`.
88///
89/// This is a no-op if:
90/// - `target_dc_id == home_dc_id` (already home)
91/// - auth was already copied in this session (tracked by `DcAuthTracker`)
92///
93/// Ported from  `Client::copy_auth_to_dc`.
94///
95/// # Where to call this
96///
97/// Call from `invoke_on_dc(target_dc_id, req)` before sending the request,
98/// so that file downloads on foreign DCs work without manual setup:
99///
100/// ```rust,ignore
101/// pub async fn invoke_on_dc<R: RemoteCall>(
102/// &self,
103/// dc_id: i32,
104/// req: &R,
105/// ) -> Result<R::Return, InvocationError> {
106/// self.copy_auth_to_dc(dc_id).await?;
107/// // ... then call the DC-specific connection
108/// }
109/// ```
110pub async fn copy_auth_to_dc<F, Fut>(
111    home_dc_id: i32,
112    target_dc_id: i32,
113    tracker: &DcAuthTracker,
114    invoke_fn: F, // calls the home DC
115    invoke_on_dc_fn: impl Fn(i32, tl::functions::auth::ImportAuthorization) -> Fut,
116) -> Result<(), InvocationError>
117where
118    F: std::future::Future<
119            Output = Result<tl::enums::auth::ExportedAuthorization, InvocationError>,
120        >,
121    Fut: std::future::Future<Output = Result<tl::enums::auth::Authorization, InvocationError>>,
122{
123    if target_dc_id == home_dc_id {
124        return Ok(());
125    }
126    if tracker.has_copied(target_dc_id) {
127        return Ok(());
128    }
129
130    // Export from home DC
131    let tl::enums::auth::ExportedAuthorization::ExportedAuthorization(exported) = invoke_fn.await?;
132
133    // Import on target DC
134    invoke_on_dc_fn(
135        target_dc_id,
136        tl::functions::auth::ImportAuthorization {
137            id: exported.id,
138            bytes: exported.bytes,
139        },
140    )
141    .await?;
142
143    tracker.mark_copied(target_dc_id);
144    Ok(())
145}
146
147// migrate_to integration patch
148//
149// The following documents what migrate_to must be changed to use
150// fallback_dc_addr() instead of a hardcoded string.
151//
152// In lib.rs, replace:
153//
154// .unwrap_or_else(|| "149.154.167.51:443".to_string())
155//
156// With:
157//
158// .unwrap_or_else(|| crate::dc_migration::fallback_dc_addr(new_dc_id).to_string())
159//
160// And add auto-migration to rpc_call_raw:
161
162/// Patch description for `rpc_call_raw` in lib.rs.
163///
164/// Replace the existing loop body:
165/// ```rust,ignore
166/// // BEFORE: only FLOOD_WAIT handled:
167/// async fn rpc_call_raw<R: RemoteCall>(&self, req: &R) -> Result<Vec<u8>, InvocationError> {
168/// let mut fail_count   = NonZeroU32::new(1).unwrap();
169/// let mut slept_so_far = Duration::default();
170/// loop {
171///     match self.do_rpc_call(req).await {
172///         Ok(body) => return Ok(body),
173///         Err(e) => {
174///             let ctx = RetryContext { fail_count, slept_so_far, error: e };
175///             match self.inner.retry_policy.should_retry(&ctx) {
176///                 ControlFlow::Continue(delay) => { sleep(delay).await; slept_so_far += delay; fail_count = fail_count.saturating_add(1); }
177///                 ControlFlow::Break(())       => return Err(ctx.error),
178///             }
179///         }
180///     }
181/// }
182/// }
183///
184/// // AFTER: MIGRATE auto-handled, RetryLoop used:
185/// async fn rpc_call_raw<R: RemoteCall>(&self, req: &R) -> Result<Vec<u8>, InvocationError> {
186/// let mut rl = RetryLoop::new(Arc::clone(&self.inner.retry_policy));
187/// loop {
188///     match self.do_rpc_call(req).await {
189///         Ok(body) => return Ok(body),
190///         Err(e) if let Some(dc_id) = e.migrate_dc_id() => {
191///             // Telegram is redirecting us to a different DC.
192///             // Migrate transparently and retry: no error surfaces to caller.
193///             self.migrate_to(dc_id).await?;
194///         }
195///         Err(e) => rl.advance(e).await?,
196///     }
197/// }
198/// }
199/// ```
200///
201/// With this change, the manual MIGRATE checks in `bot_sign_in`,
202/// `request_login_code`, and `sign_in` can be deleted.
203pub const MIGRATE_PATCH_DESCRIPTION: &str = "see doc comment above";
204
205// Tests
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    // fallback_dc_addr
212
213    #[test]
214    fn known_dcs_return_correct_ips() {
215        assert_eq!(fallback_dc_addr(1), "149.154.175.53:443");
216        assert_eq!(fallback_dc_addr(2), "149.154.167.51:443");
217        assert_eq!(fallback_dc_addr(3), "149.154.175.100:443");
218        assert_eq!(fallback_dc_addr(4), "149.154.167.91:443");
219        assert_eq!(fallback_dc_addr(5), "91.108.56.130:443");
220    }
221
222    #[test]
223    fn unknown_dc_falls_back_to_dc2() {
224        assert_eq!(fallback_dc_addr(99), "149.154.167.51:443");
225    }
226
227    #[test]
228    fn default_dc_addresses_has_five_entries() {
229        let addrs = default_dc_addresses();
230        assert_eq!(addrs.len(), 5);
231        // DCs 1-5 are all present
232        for id in 1..=5_i32 {
233            assert!(addrs.iter().any(|(dc_id, _)| *dc_id == id));
234        }
235    }
236
237    // DcAuthTracker
238
239    #[test]
240    fn tracker_starts_empty() {
241        let t = DcAuthTracker::new();
242        assert!(!t.has_copied(2));
243        assert!(!t.has_copied(4));
244    }
245
246    #[test]
247    fn tracker_marks_and_checks() {
248        let t = DcAuthTracker::new();
249        t.mark_copied(4);
250        assert!(t.has_copied(4));
251        assert!(!t.has_copied(2));
252    }
253
254    #[test]
255    fn tracker_marks_multiple_dcs() {
256        let t = DcAuthTracker::new();
257        t.mark_copied(2);
258        t.mark_copied(4);
259        t.mark_copied(5);
260        assert!(t.has_copied(2));
261        assert!(t.has_copied(4));
262        assert!(t.has_copied(5));
263        assert!(!t.has_copied(1));
264        assert!(!t.has_copied(3));
265    }
266
267    // migrate_dc_id detection (also in retry.rs but sanity check here)
268
269    #[test]
270    fn rpc_error_migrate_detection_all_variants() {
271        use crate::errors::RpcError;
272
273        for name in &[
274            "PHONE_MIGRATE",
275            "NETWORK_MIGRATE",
276            "FILE_MIGRATE",
277            "USER_MIGRATE",
278        ] {
279            let e = RpcError {
280                code: 303,
281                name: name.to_string(),
282                value: Some(4),
283            };
284            assert_eq!(e.migrate_dc_id(), Some(4), "failed for {name}");
285        }
286    }
287
288    #[test]
289    fn invocation_error_migrate_dc_id_delegates() {
290        use crate::errors::{InvocationError, RpcError};
291        let e = InvocationError::Rpc(RpcError {
292            code: 303,
293            name: "PHONE_MIGRATE".into(),
294            value: Some(5),
295        });
296        assert_eq!(e.migrate_dc_id(), Some(5));
297    }
298}