Skip to main content

layer_client/
errors.rs

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