Skip to main content

layer_client/
builder.rs

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