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}