Skip to main content

layer_client/
errors.rs

1//! Error types for layer-client.
2//!
3//! Mirrors `grammers_mtsender` error hierarchy for API compatibility.
4
5use std::{fmt, io};
6
7// ─── RpcError ─────────────────────────────────────────────────────────────────
8
9/// An error returned by Telegram's servers in response to an RPC call.
10///
11/// Numeric values are stripped from the name and placed in [`RpcError::value`].
12///
13/// # Example
14/// `FLOOD_WAIT_30` → `RpcError { code: 420, name: "FLOOD_WAIT", value: Some(30) }`
15#[derive(Clone, Debug, PartialEq)]
16pub struct RpcError {
17    /// HTTP-like status code.
18    pub code: i32,
19    /// Error name in SCREAMING_SNAKE_CASE with digits removed.
20    pub name: String,
21    /// Numeric suffix extracted from the name, if any.
22    pub value: Option<u32>,
23}
24
25impl fmt::Display for RpcError {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "RPC {}: {}", self.code, self.name)?;
28        if let Some(v) = self.value {
29            write!(f, " (value: {v})")?;
30        }
31        Ok(())
32    }
33}
34
35impl std::error::Error for RpcError {}
36
37impl RpcError {
38    /// Parse a raw Telegram error message like `"FLOOD_WAIT_30"` into an `RpcError`.
39    pub fn from_telegram(code: i32, message: &str) -> Self {
40        // Try to find a numeric suffix after the last underscore.
41        // e.g. "FLOOD_WAIT_30" → name = "FLOOD_WAIT", value = Some(30)
42        if let Some(idx) = message.rfind('_') {
43            let suffix = &message[idx + 1..];
44            if !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()) {
45                if let Ok(v) = suffix.parse::<u32>() {
46                    let name = message[..idx].to_string();
47                    return Self { code, name, value: Some(v) };
48                }
49            }
50        }
51        Self { code, name: message.to_string(), value: None }
52    }
53
54    /// Match on the error name, with optional wildcard prefix/suffix `'*'`.
55    ///
56    /// # Examples
57    /// - `err.is("FLOOD_WAIT")` — exact match
58    /// - `err.is("PHONE_CODE_*")` — starts-with match  
59    /// - `err.is("*_INVALID")` — ends-with match
60    pub fn is(&self, pattern: &str) -> bool {
61        if let Some(prefix) = pattern.strip_suffix('*') {
62            self.name.starts_with(prefix)
63        } else if let Some(suffix) = pattern.strip_prefix('*') {
64            self.name.ends_with(suffix)
65        } else {
66            self.name == pattern
67        }
68    }
69
70    /// Returns the flood-wait duration in seconds, if this is a FLOOD_WAIT error.
71    pub fn flood_wait_seconds(&self) -> Option<u64> {
72        if self.code == 420 && self.name == "FLOOD_WAIT" {
73            self.value.map(|v| v as u64)
74        } else {
75            None
76        }
77    }
78}
79
80// ─── InvocationError ──────────────────────────────────────────────────────────
81
82/// The error type returned from any `Client` method that talks to Telegram.
83#[derive(Debug)]
84pub enum InvocationError {
85    /// Telegram rejected the request.
86    Rpc(RpcError),
87    /// Network / I/O failure.
88    Io(io::Error),
89    /// Response deserialization failed.
90    Deserialize(String),
91    /// The request was dropped (e.g. sender task shut down).
92    Dropped,
93    /// DC migration required — internal, automatically handled by [`crate::Client`].
94    Migrate(i32),
95}
96
97impl fmt::Display for InvocationError {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::Rpc(e)          => write!(f, "{e}"),
101            Self::Io(e)           => write!(f, "I/O error: {e}"),
102            Self::Deserialize(s)  => write!(f, "deserialize error: {s}"),
103            Self::Dropped         => write!(f, "request dropped"),
104            Self::Migrate(dc)     => write!(f, "DC migration to {dc}"),
105        }
106    }
107}
108
109impl std::error::Error for InvocationError {}
110
111impl From<io::Error> for InvocationError {
112    fn from(e: io::Error) -> Self { Self::Io(e) }
113}
114
115impl From<layer_tl_types::deserialize::Error> for InvocationError {
116    fn from(e: layer_tl_types::deserialize::Error) -> Self { Self::Deserialize(e.to_string()) }
117}
118
119impl InvocationError {
120    /// Returns `true` if this is the named RPC error (supports `'*'` wildcards).
121    pub fn is(&self, pattern: &str) -> bool {
122        match self {
123            Self::Rpc(e) => e.is(pattern),
124            _            => false,
125        }
126    }
127
128    /// If this is a FLOOD_WAIT error, returns how many seconds to wait.
129    pub fn flood_wait_seconds(&self) -> Option<u64> {
130        match self {
131            Self::Rpc(e) => e.flood_wait_seconds(),
132            _            => None,
133        }
134    }
135}
136
137// ─── SignInError ──────────────────────────────────────────────────────────────
138
139/// Errors returned by [`crate::Client::sign_in`].
140#[derive(Debug)]
141pub enum SignInError {
142    /// The phone number is new — must sign up via the official Telegram app first.
143    SignUpRequired,
144    /// 2FA is enabled; the contained token must be passed to [`crate::Client::check_password`].
145    PasswordRequired(PasswordToken),
146    /// The code entered was wrong or has expired.
147    InvalidCode,
148    /// Any other error.
149    Other(InvocationError),
150}
151
152impl fmt::Display for SignInError {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            Self::SignUpRequired        => write!(f, "sign up required — use official Telegram app"),
156            Self::PasswordRequired(_)  => write!(f, "2FA password required"),
157            Self::InvalidCode          => write!(f, "invalid or expired code"),
158            Self::Other(e)             => write!(f, "{e}"),
159        }
160    }
161}
162
163impl std::error::Error for SignInError {}
164
165impl From<InvocationError> for SignInError {
166    fn from(e: InvocationError) -> Self { Self::Other(e) }
167}
168
169// ─── PasswordToken ────────────────────────────────────────────────────────────
170
171/// Opaque 2FA challenge token returned in [`SignInError::PasswordRequired`].
172///
173/// Pass to [`crate::Client::check_password`] together with the user's password.
174pub struct PasswordToken {
175    pub(crate) password: layer_tl_types::types::account::Password,
176}
177
178impl PasswordToken {
179    /// The password hint set by the account owner, if any.
180    pub fn hint(&self) -> Option<&str> {
181        self.password.hint.as_deref()
182    }
183}
184
185impl fmt::Debug for PasswordToken {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        write!(f, "PasswordToken {{ hint: {:?} }}", self.hint())
188    }
189}
190
191// ─── LoginToken ───────────────────────────────────────────────────────────────
192
193/// Opaque token returned by [`crate::Client::request_login_code`].
194///
195/// Pass to [`crate::Client::sign_in`] with the received code.
196pub struct LoginToken {
197    pub(crate) phone:           String,
198    pub(crate) phone_code_hash: String,
199}