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                && let Ok(v) = suffix.parse::<u32>() {
46                let name = message[..idx].to_string();
47                return Self { code, name, value: Some(v) };
48            }
49        }
50        Self { code, name: message.to_string(), value: None }
51    }
52
53    /// Match on the error name, with optional wildcard prefix/suffix `'*'`.
54    ///
55    /// # Examples
56    /// - `err.is("FLOOD_WAIT")` — exact match
57    /// - `err.is("PHONE_CODE_*")` — starts-with match  
58    /// - `err.is("*_INVALID")` — ends-with match
59    pub fn is(&self, pattern: &str) -> bool {
60        if let Some(prefix) = pattern.strip_suffix('*') {
61            self.name.starts_with(prefix)
62        } else if let Some(suffix) = pattern.strip_prefix('*') {
63            self.name.ends_with(suffix)
64        } else {
65            self.name == pattern
66        }
67    }
68
69    /// Returns the flood-wait duration in seconds, if this is a FLOOD_WAIT error.
70    pub fn flood_wait_seconds(&self) -> Option<u64> {
71        if self.code == 420 && self.name == "FLOOD_WAIT" {
72            self.value.map(|v| v as u64)
73        } else {
74            None
75        }
76    }
77}
78
79// ─── InvocationError ──────────────────────────────────────────────────────────
80
81/// The error type returned from any `Client` method that talks to Telegram.
82#[derive(Debug)]
83pub enum InvocationError {
84    /// Telegram rejected the request.
85    Rpc(RpcError),
86    /// Network / I/O failure.
87    Io(io::Error),
88    /// Response deserialization failed.
89    Deserialize(String),
90    /// The request was dropped (e.g. sender task shut down).
91    Dropped,
92    /// DC migration required — internal, automatically handled by [`crate::Client`].
93    Migrate(i32),
94}
95
96impl fmt::Display for InvocationError {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match self {
99            Self::Rpc(e)          => write!(f, "{e}"),
100            Self::Io(e)           => write!(f, "I/O error: {e}"),
101            Self::Deserialize(s)  => write!(f, "deserialize error: {s}"),
102            Self::Dropped         => write!(f, "request dropped"),
103            Self::Migrate(dc)     => write!(f, "DC migration to {dc}"),
104        }
105    }
106}
107
108impl std::error::Error for InvocationError {}
109
110impl From<io::Error> for InvocationError {
111    fn from(e: io::Error) -> Self { Self::Io(e) }
112}
113
114impl From<layer_tl_types::deserialize::Error> for InvocationError {
115    fn from(e: layer_tl_types::deserialize::Error) -> Self { Self::Deserialize(e.to_string()) }
116}
117
118impl InvocationError {
119    /// Returns `true` if this is the named RPC error (supports `'*'` wildcards).
120    pub fn is(&self, pattern: &str) -> bool {
121        match self {
122            Self::Rpc(e) => e.is(pattern),
123            _            => false,
124        }
125    }
126
127    /// If this is a FLOOD_WAIT error, returns how many seconds to wait.
128    pub fn flood_wait_seconds(&self) -> Option<u64> {
129        match self {
130            Self::Rpc(e) => e.flood_wait_seconds(),
131            _            => None,
132        }
133    }
134}
135
136// ─── SignInError ──────────────────────────────────────────────────────────────
137
138/// Errors returned by [`crate::Client::sign_in`].
139#[derive(Debug)]
140pub enum SignInError {
141    /// The phone number is new — must sign up via the official Telegram app first.
142    SignUpRequired,
143    /// 2FA is enabled; the contained token must be passed to [`crate::Client::check_password`].
144    PasswordRequired(Box<PasswordToken>),
145    /// The code entered was wrong or has expired.
146    InvalidCode,
147    /// Any other error.
148    Other(InvocationError),
149}
150
151impl fmt::Display for SignInError {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        match self {
154            Self::SignUpRequired        => write!(f, "sign up required — use official Telegram app"),
155            Self::PasswordRequired(_)  => write!(f, "2FA password required"),
156            Self::InvalidCode          => write!(f, "invalid or expired code"),
157            Self::Other(e)             => write!(f, "{e}"),
158        }
159    }
160}
161
162impl std::error::Error for SignInError {}
163
164impl From<InvocationError> for SignInError {
165    fn from(e: InvocationError) -> Self { Self::Other(e) }
166}
167
168// ─── PasswordToken ────────────────────────────────────────────────────────────
169
170/// Opaque 2FA challenge token returned in [`SignInError::PasswordRequired`].
171///
172/// Pass to [`crate::Client::check_password`] together with the user's password.
173pub struct PasswordToken {
174    pub(crate) password: layer_tl_types::types::account::Password,
175}
176
177impl PasswordToken {
178    /// The password hint set by the account owner, if any.
179    pub fn hint(&self) -> Option<&str> {
180        self.password.hint.as_deref()
181    }
182}
183
184impl fmt::Debug for PasswordToken {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(f, "PasswordToken {{ hint: {:?} }}", self.hint())
187    }
188}
189
190// ─── LoginToken ───────────────────────────────────────────────────────────────
191
192/// Opaque token returned by [`crate::Client::request_login_code`].
193///
194/// Pass to [`crate::Client::sign_in`] with the received code.
195pub struct LoginToken {
196    pub(crate) phone:           String,
197    pub(crate) phone_code_hash: String,
198}