layer_client/builder.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//! [`ClientBuilder`] for constructing a [`Config`] and connecting.
20//!
21//! # Example
22//! ```rust,no_run
23//! use layer_client::Client;
24//!
25//! #[tokio::main]
26//! async fn main() -> anyhow::Result<()> {
27//! let (client, _shutdown) = Client::builder()
28//! .api_id(12345)
29//! .api_hash("abc123")
30//! .session("my.session")
31//! .catch_up(true)
32//! .device_model("MyApp on Linux")
33//! .system_version("Ubuntu 24.04")
34//! .app_version("0.1.0")
35//! .lang_code("en")
36//! .connect().await?;
37//! Ok(())
38//! }
39//! ```
40//!
41//! Use `.session_string(s)` instead of `.session(path)` for portable base64 sessions:
42//! ```rust,no_run
43//! # use layer_client::Client;
44//! # #[tokio::main] async fn main() -> anyhow::Result<()> {
45//! let (client, _shutdown) = Client::builder()
46//! .api_id(12345)
47//! .api_hash("abc123")
48//! .session_string(std::env::var("SESSION").unwrap_or_default())
49//! .connect().await?;
50//! # Ok(()) }
51//! ```
52
53use std::sync::Arc;
54
55use crate::{
56 Client, Config, InvocationError, ShutdownToken, TransportKind,
57 restart::{ConnectionRestartPolicy, NeverRestart},
58 retry::{AutoSleep, RetryPolicy},
59 session_backend::{BinaryFileBackend, InMemoryBackend, SessionBackend, StringSessionBackend},
60 socks5::Socks5Config,
61};
62
63/// Fluent builder for [`Config`] + [`Client::connect`].
64///
65/// Obtain one via [`Client::builder()`].
66pub struct ClientBuilder {
67 api_id: i32,
68 api_hash: String,
69 dc_addr: Option<String>,
70 retry_policy: Arc<dyn RetryPolicy>,
71 restart_policy: Arc<dyn ConnectionRestartPolicy>,
72 socks5: Option<Socks5Config>,
73 mtproxy: Option<crate::proxy::MtProxyConfig>,
74 allow_ipv6: bool,
75 transport: TransportKind,
76 session_backend: Arc<dyn SessionBackend>,
77 catch_up: bool,
78 device_model: String,
79 system_version: String,
80 app_version: String,
81 system_lang_code: String,
82 lang_pack: String,
83 lang_code: String,
84}
85
86impl Default for ClientBuilder {
87 fn default() -> Self {
88 Self {
89 api_id: 0,
90 api_hash: String::new(),
91 dc_addr: None,
92 retry_policy: Arc::new(AutoSleep::default()),
93 restart_policy: Arc::new(NeverRestart),
94 socks5: None,
95 mtproxy: None,
96 allow_ipv6: false,
97 transport: TransportKind::Abridged,
98 session_backend: Arc::new(BinaryFileBackend::new("layer.session")),
99 catch_up: false,
100 device_model: "Linux".to_string(),
101 system_version: "1.0".to_string(),
102 app_version: env!("CARGO_PKG_VERSION").to_string(),
103 system_lang_code: "en".to_string(),
104 lang_pack: String::new(),
105 lang_code: "en".to_string(),
106 }
107 }
108}
109
110impl ClientBuilder {
111 // Credentials
112
113 /// Set the Telegram API ID (from <https://my.telegram.org>).
114 pub fn api_id(mut self, id: i32) -> Self {
115 self.api_id = id;
116 self
117 }
118
119 /// Set the Telegram API hash (from <https://my.telegram.org>).
120 pub fn api_hash(mut self, hash: impl Into<String>) -> Self {
121 self.api_hash = hash.into();
122 self
123 }
124
125 // Session
126
127 /// Use a binary file session at `path`.
128 ///
129 /// Mutually exclusive with [`session_string`](Self::session_string) and
130 /// [`in_memory`](Self::in_memory): last call wins.
131 pub fn session(mut self, path: impl AsRef<std::path::Path>) -> Self {
132 self.session_backend = Arc::new(BinaryFileBackend::new(path.as_ref()));
133 self
134 }
135
136 /// Use a portable base64 string session.
137 ///
138 /// Pass an empty string to start fresh: the exported session string
139 /// from [`Client::export_session_string`] can be injected here directly
140 /// (e.g. via an environment variable).
141 ///
142 /// Mutually exclusive with [`session`](Self::session) and
143 /// [`in_memory`](Self::in_memory): last call wins.
144 pub fn session_string(mut self, s: impl Into<String>) -> Self {
145 self.session_backend = Arc::new(StringSessionBackend::new(s));
146 self
147 }
148
149 /// Use a non-persistent in-memory session (useful for tests).
150 ///
151 /// Mutually exclusive with [`session`](Self::session) and
152 /// [`session_string`](Self::session_string): last call wins.
153 pub fn in_memory(mut self) -> Self {
154 self.session_backend = Arc::new(InMemoryBackend::new());
155 self
156 }
157
158 /// Inject a fully custom [`SessionBackend`] implementation.
159 ///
160 /// Useful for [`LibSqlBackend`] (bundled SQLite, no system dep) or any
161 /// custom persistence layer:
162 /// ```rust,no_run
163 /// # use layer_client::{Client};
164 /// # #[cfg(feature = "libsql-session")] {
165 /// # use layer_client::LibSqlBackend;
166 /// use std::sync::Arc;
167 /// let (client, _) = Client::builder()
168 /// .api_id(12345).api_hash("abc")
169 /// .session_backend(Arc::new(LibSqlBackend::new("my.db")))
170 /// .connect().await?;
171 /// # }
172 /// ```
173 pub fn session_backend(mut self, backend: Arc<dyn SessionBackend>) -> Self {
174 self.session_backend = backend;
175 self
176 }
177
178 // Update catch-up
179
180 /// When `true`, replay missed updates via `updates.getDifference` on connect.
181 ///
182 /// Default: `false`.
183 pub fn catch_up(mut self, enabled: bool) -> Self {
184 self.catch_up = enabled;
185 self
186 }
187
188 // Network
189
190 /// Override the first DC address (e.g. `"149.154.167.51:443"`).
191 pub fn dc_addr(mut self, addr: impl Into<String>) -> Self {
192 self.dc_addr = Some(addr.into());
193 self
194 }
195
196 /// Route all connections through a SOCKS5 proxy.
197 pub fn socks5(mut self, proxy: Socks5Config) -> Self {
198 self.socks5 = Some(proxy);
199 self
200 }
201
202 /// Route all connections through an MTProxy.
203 ///
204 /// The proxy `transport` is set automatically from the secret prefix;
205 /// you do not need to also call `.transport()`.
206 /// Build the [`MtProxyConfig`] with [`crate::parse_proxy_link`].
207 pub fn mtproxy(mut self, proxy: crate::proxy::MtProxyConfig) -> Self {
208 // Override transport to match what the proxy requires.
209 self.transport = proxy.transport.clone();
210 self.mtproxy = Some(proxy);
211 self
212 }
213
214 /// Set an MTProxy from a `https://t.me/proxy?...` or `tg://proxy?...` link.
215 ///
216 /// Empty string is a no-op; proxy stays unset.
217 /// Transport is selected from the secret prefix automatically.
218 pub fn proxy_link(mut self, url: &str) -> Self {
219 if url.is_empty() {
220 return self;
221 }
222 if let Some(cfg) = crate::proxy::parse_proxy_link(url) {
223 self.transport = cfg.transport.clone();
224 self.mtproxy = Some(cfg);
225 }
226 self
227 }
228
229 /// Allow IPv6 DC addresses (default: `false`).
230 pub fn allow_ipv6(mut self, allow: bool) -> Self {
231 self.allow_ipv6 = allow;
232 self
233 }
234
235 /// Choose the MTProto transport framing (default: [`TransportKind::Abridged`]).
236 pub fn transport(mut self, kind: TransportKind) -> Self {
237 self.transport = kind;
238 self
239 }
240
241 // Retry
242
243 /// Override the flood-wait / retry policy.
244 pub fn retry_policy(mut self, policy: Arc<dyn RetryPolicy>) -> Self {
245 self.retry_policy = policy;
246 self
247 }
248
249 pub fn restart_policy(mut self, policy: Arc<dyn ConnectionRestartPolicy>) -> Self {
250 self.restart_policy = policy;
251 self
252 }
253
254 // InitConnection identity
255
256 /// Set the device model string sent in `InitConnection` (default: `"Linux"`).
257 ///
258 /// This shows up in Telegram's active sessions list as the device name.
259 pub fn device_model(mut self, model: impl Into<String>) -> Self {
260 self.device_model = model.into();
261 self
262 }
263
264 /// Set the system/OS version string sent in `InitConnection` (default: `"1.0"`).
265 pub fn system_version(mut self, version: impl Into<String>) -> Self {
266 self.system_version = version.into();
267 self
268 }
269
270 /// Set the app version string sent in `InitConnection` (default: crate version from `CARGO_PKG_VERSION`).
271 pub fn app_version(mut self, version: impl Into<String>) -> Self {
272 self.app_version = version.into();
273 self
274 }
275
276 /// Set the system language code sent in `InitConnection` (default: `"en"`).
277 pub fn system_lang_code(mut self, code: impl Into<String>) -> Self {
278 self.system_lang_code = code.into();
279 self
280 }
281
282 /// Set the language pack name sent in `InitConnection` (default: `""`).
283 pub fn lang_pack(mut self, pack: impl Into<String>) -> Self {
284 self.lang_pack = pack.into();
285 self
286 }
287
288 /// Set the language code sent in `InitConnection` (default: `"en"`).
289 pub fn lang_code(mut self, code: impl Into<String>) -> Self {
290 self.lang_code = code.into();
291 self
292 }
293
294 // Terminal
295
296 /// Build the [`Config`] without connecting.
297 pub fn build(self) -> Result<Config, BuilderError> {
298 if self.api_id == 0 {
299 return Err(BuilderError::MissingApiId);
300 }
301 if self.api_hash.is_empty() {
302 return Err(BuilderError::MissingApiHash);
303 }
304 Ok(Config {
305 api_id: self.api_id,
306 api_hash: self.api_hash,
307 dc_addr: self.dc_addr,
308 retry_policy: self.retry_policy,
309 restart_policy: self.restart_policy,
310 socks5: self.socks5,
311 mtproxy: self.mtproxy,
312 allow_ipv6: self.allow_ipv6,
313 transport: self.transport,
314 session_backend: self.session_backend,
315 catch_up: self.catch_up,
316 device_model: self.device_model,
317 system_version: self.system_version,
318 app_version: self.app_version,
319 system_lang_code: self.system_lang_code,
320 lang_pack: self.lang_pack,
321 lang_code: self.lang_code,
322 })
323 }
324
325 /// Build and connect in one step.
326 ///
327 /// Returns `Err(BuilderError::MissingApiId)` / `Err(BuilderError::MissingApiHash)`
328 /// before attempting any network I/O if the required fields are absent.
329 pub async fn connect(self) -> Result<(Client, ShutdownToken), BuilderError> {
330 let cfg = self.build()?;
331 Client::connect(cfg).await.map_err(BuilderError::Connect)
332 }
333}
334
335// BuilderError
336
337/// Errors that can be returned by [`ClientBuilder::build`] or
338/// [`ClientBuilder::connect`].
339#[derive(Debug)]
340pub enum BuilderError {
341 /// `api_id` was not set (or left at 0).
342 MissingApiId,
343 /// `api_hash` was not set (or left empty).
344 MissingApiHash,
345 /// The underlying [`Client::connect`] call failed.
346 Connect(InvocationError),
347}
348
349impl std::fmt::Display for BuilderError {
350 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 match self {
352 Self::MissingApiId => f.write_str("ClientBuilder: api_id not set"),
353 Self::MissingApiHash => f.write_str("ClientBuilder: api_hash not set"),
354 Self::Connect(e) => write!(f, "ClientBuilder: connect failed: {e}"),
355 }
356 }
357}
358
359impl std::error::Error for BuilderError {
360 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
361 match self {
362 Self::Connect(e) => Some(e),
363 _ => None,
364 }
365 }
366}