Skip to main content

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