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}