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//!     .connect().await?;
15//! Ok(())
16//! }
17//! ```
18//!
19//! Use `.session_string(s)` instead of `.session(path)` for portable base64 sessions:
20//! ```rust,no_run
21//! # use layer_client::Client;
22//! # #[tokio::main] async fn main() -> anyhow::Result<()> {
23//! let (client, _shutdown) = Client::builder()
24//! .api_id(12345)
25//! .api_hash("abc123")
26//! .session_string(std::env::var("SESSION").unwrap_or_default())
27//! .connect().await?;
28//! # Ok(()) }
29//! ```
30
31use std::sync::Arc;
32
33use crate::{
34    Client, Config, InvocationError, ShutdownToken, TransportKind,
35    restart::{ConnectionRestartPolicy, NeverRestart},
36    retry::{AutoSleep, RetryPolicy},
37    session_backend::{BinaryFileBackend, InMemoryBackend, SessionBackend, StringSessionBackend},
38    socks5::Socks5Config,
39};
40
41/// Fluent builder for [`Config`] + [`Client::connect`].
42///
43/// Obtain one via [`Client::builder()`].
44pub struct ClientBuilder {
45    api_id: i32,
46    api_hash: String,
47    dc_addr: Option<String>,
48    retry_policy: Arc<dyn RetryPolicy>,
49    restart_policy: Arc<dyn ConnectionRestartPolicy>,
50    socks5: Option<Socks5Config>,
51    mtproxy: Option<crate::proxy::MtProxyConfig>,
52    allow_ipv6: bool,
53    transport: TransportKind,
54    session_backend: Arc<dyn SessionBackend>,
55    catch_up: bool,
56}
57
58impl Default for ClientBuilder {
59    fn default() -> Self {
60        Self {
61            api_id: 0,
62            api_hash: String::new(),
63            dc_addr: None,
64            retry_policy: Arc::new(AutoSleep::default()),
65            restart_policy: Arc::new(NeverRestart),
66            socks5: None,
67            mtproxy: None,
68            allow_ipv6: false,
69            transport: TransportKind::Abridged,
70            session_backend: Arc::new(BinaryFileBackend::new("layer.session")),
71            catch_up: false,
72        }
73    }
74}
75
76impl ClientBuilder {
77    // Credentials
78
79    /// Set the Telegram API ID (from <https://my.telegram.org>).
80    pub fn api_id(mut self, id: i32) -> Self {
81        self.api_id = id;
82        self
83    }
84
85    /// Set the Telegram API hash (from <https://my.telegram.org>).
86    pub fn api_hash(mut self, hash: impl Into<String>) -> Self {
87        self.api_hash = hash.into();
88        self
89    }
90
91    // Session
92
93    /// Use a binary file session at `path`.
94    ///
95    /// Mutually exclusive with [`session_string`](Self::session_string) and
96    /// [`in_memory`](Self::in_memory): last call wins.
97    pub fn session(mut self, path: impl AsRef<std::path::Path>) -> Self {
98        self.session_backend = Arc::new(BinaryFileBackend::new(path.as_ref()));
99        self
100    }
101
102    /// Use a portable base64 string session.
103    ///
104    /// Pass an empty string to start fresh: the exported session string
105    /// from [`Client::export_session_string`] can be injected here directly
106    /// (e.g. via an environment variable).
107    ///
108    /// Mutually exclusive with [`session`](Self::session) and
109    /// [`in_memory`](Self::in_memory): last call wins.
110    pub fn session_string(mut self, s: impl Into<String>) -> Self {
111        self.session_backend = Arc::new(StringSessionBackend::new(s));
112        self
113    }
114
115    /// Use a non-persistent in-memory session (useful for tests).
116    ///
117    /// Mutually exclusive with [`session`](Self::session) and
118    /// [`session_string`](Self::session_string): last call wins.
119    pub fn in_memory(mut self) -> Self {
120        self.session_backend = Arc::new(InMemoryBackend::new());
121        self
122    }
123
124    /// Inject a fully custom [`SessionBackend`] implementation.
125    ///
126    /// Useful for [`LibSqlBackend`] (bundled SQLite, no system dep) or any
127    /// custom persistence layer:
128    /// ```rust,no_run
129    /// # use layer_client::{Client};
130    /// # #[cfg(feature = "libsql-session")] {
131    /// # use layer_client::LibSqlBackend;
132    /// use std::sync::Arc;
133    /// let (client, _) = Client::builder()
134    /// .api_id(12345).api_hash("abc")
135    /// .session_backend(Arc::new(LibSqlBackend::new("my.db")))
136    /// .connect().await?;
137    /// # }
138    /// ```
139    pub fn session_backend(mut self, backend: Arc<dyn SessionBackend>) -> Self {
140        self.session_backend = backend;
141        self
142    }
143
144    // Update catch-up
145
146    /// When `true`, replay missed updates via `updates.getDifference` on connect.
147    ///
148    /// Default: `false`.
149    pub fn catch_up(mut self, enabled: bool) -> Self {
150        self.catch_up = enabled;
151        self
152    }
153
154    // Network
155
156    /// Override the first DC address (e.g. `"149.154.167.51:443"`).
157    pub fn dc_addr(mut self, addr: impl Into<String>) -> Self {
158        self.dc_addr = Some(addr.into());
159        self
160    }
161
162    /// Route all connections through a SOCKS5 proxy.
163    pub fn socks5(mut self, proxy: Socks5Config) -> Self {
164        self.socks5 = Some(proxy);
165        self
166    }
167
168    /// Route all connections through an MTProxy.
169    ///
170    /// The proxy `transport` is set automatically from the secret prefix;
171    /// you do not need to also call `.transport()`.
172    /// Build the [`MtProxyConfig`] with [`crate::parse_proxy_link`].
173    pub fn mtproxy(mut self, proxy: crate::proxy::MtProxyConfig) -> Self {
174        // Override transport to match what the proxy requires.
175        self.transport = proxy.transport.clone();
176        self.mtproxy = Some(proxy);
177        self
178    }
179
180    /// Allow IPv6 DC addresses (default: `false`).
181    pub fn allow_ipv6(mut self, allow: bool) -> Self {
182        self.allow_ipv6 = allow;
183        self
184    }
185
186    /// Choose the MTProto transport framing (default: [`TransportKind::Abridged`]).
187    pub fn transport(mut self, kind: TransportKind) -> Self {
188        self.transport = kind;
189        self
190    }
191
192    // Retry
193
194    /// Override the flood-wait / retry policy.
195    pub fn retry_policy(mut self, policy: Arc<dyn RetryPolicy>) -> Self {
196        self.retry_policy = policy;
197        self
198    }
199
200    pub fn restart_policy(mut self, policy: Arc<dyn ConnectionRestartPolicy>) -> Self {
201        self.restart_policy = policy;
202        self
203    }
204
205    // Terminal
206
207    /// Build the [`Config`] without connecting.
208    pub fn build(self) -> Result<Config, BuilderError> {
209        if self.api_id == 0 {
210            return Err(BuilderError::MissingApiId);
211        }
212        if self.api_hash.is_empty() {
213            return Err(BuilderError::MissingApiHash);
214        }
215        Ok(Config {
216            api_id: self.api_id,
217            api_hash: self.api_hash,
218            dc_addr: self.dc_addr,
219            retry_policy: self.retry_policy,
220            restart_policy: self.restart_policy,
221            socks5: self.socks5,
222            mtproxy: self.mtproxy,
223            allow_ipv6: self.allow_ipv6,
224            transport: self.transport,
225            session_backend: self.session_backend,
226            catch_up: self.catch_up,
227        })
228    }
229
230    /// Build and connect in one step.
231    ///
232    /// Returns `Err(BuilderError::MissingApiId)` / `Err(BuilderError::MissingApiHash)`
233    /// before attempting any network I/O if the required fields are absent.
234    pub async fn connect(self) -> Result<(Client, ShutdownToken), BuilderError> {
235        let cfg = self.build()?;
236        Client::connect(cfg).await.map_err(BuilderError::Connect)
237    }
238}
239
240// BuilderError
241
242/// Errors that can be returned by [`ClientBuilder::build`] or
243/// [`ClientBuilder::connect`].
244#[derive(Debug)]
245pub enum BuilderError {
246    /// `api_id` was not set (or left at 0).
247    MissingApiId,
248    /// `api_hash` was not set (or left empty).
249    MissingApiHash,
250    /// The underlying [`Client::connect`] call failed.
251    Connect(InvocationError),
252}
253
254impl std::fmt::Display for BuilderError {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        match self {
257            Self::MissingApiId => f.write_str("ClientBuilder: api_id not set"),
258            Self::MissingApiHash => f.write_str("ClientBuilder: api_hash not set"),
259            Self::Connect(e) => write!(f, "ClientBuilder: connect failed: {e}"),
260        }
261    }
262}
263
264impl std::error::Error for BuilderError {
265    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
266        match self {
267            Self::Connect(e) => Some(e),
268            _ => None,
269        }
270    }
271}