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}