Skip to main content

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}