td_client/session.rs
1//! Session ownership, authentication, and ordered update consumption.
2//!
3//! Construct with [`Session::bot`] for a bot token, or [`Session::open`] to handle
4//! authorization yourself. Each owner exposes cloneable request-only [`Client`]s.
5//! Keep receiving application updates after authentication, and finish with
6//! [`Session::close`]. See the [crate guide](crate) for setup and cleanup.
7//!
8//! # Manual authorization
9//!
10//! Read [`Session::recv_auth`] until `TDLib` reports readiness. Match the generated
11//! authorization state and send the corresponding request through [`Client::send`]:
12//! for example, `setAuthenticationPhoneNumber`, `checkAuthenticationCode`, or
13//! `checkAuthenticationPassword`. Other states may require registration, email,
14//! device confirmation, or application-specific interaction. Consult the generated
15//! state documentation rather than assuming a fixed phone/code/password sequence.
16//!
17//! Parameter setup has already happened when `open` returns, although an earlier
18//! `authorizationStateWaitTdlibParameters` update may still be queued.
19//! Non-auth updates encountered during authentication are buffered for later
20//! [`Session::recv`] calls. The auth stream is not a replayable state snapshot.
21//!
22//! # Session storage
23//!
24//! Use a separate session directory for each account, and do not open one directory
25//! concurrently from multiple clients or processes. Existing authorization can be
26//! reused; changing a token does not switch the account in an authorized directory.
27//! Protect session files as credentials and configure database encryption through
28//! the generated parameters when required.
29
30use std::collections::VecDeque;
31use std::path::Path;
32use std::sync::Arc;
33
34use tokio::sync::mpsc;
35
36use td_types::enums::{AuthorizationState, Update};
37use td_types::fns;
38
39use crate::client::Client;
40use crate::connection::Connection;
41use crate::error::{Error, Result};
42use crate::runtime;
43
44/// The unique owner of one native `TDLib` session.
45///
46/// Use [`client`](Self::client) to issue requests without borrowing the update
47/// consumer. This type is intentionally not `Clone`; graceful closure consumes it.
48///
49/// # Lifecycle
50///
51/// Always drive [`close`](Self::close) to completion before process exit.
52/// Dropping the owner revokes new client requests but performs no native close,
53/// blocking wait, or join. In-flight native operations are not automatically undone.
54#[must_use = "call close().await to close TDLib cleanly"]
55pub struct Session {
56 connection: Arc<Connection>,
57 updates: mpsc::UnboundedReceiver<Update>,
58 buffered: VecDeque<Update>,
59 closed: bool,
60}
61
62impl Session {
63 /// Creates a native client session and applies the supplied `TDLib` parameters.
64 ///
65 /// This does not complete authorization. Use [`recv_auth`](Self::recv_auth)
66 /// and generated authentication requests, or construct with [`bot`](Self::bot).
67 /// [`parameters`] supplies editable defaults for the generated parameter struct.
68 ///
69 /// # Errors
70 ///
71 /// Returns the parameter request's error. Before returning a failure, attempts
72 /// graceful closure and preserves the original error if cleanup also fails.
73 ///
74 /// # Cancellation
75 ///
76 /// Drive construction to completion. Dropping this future after native creation
77 /// abandons the owner without completing graceful closure.
78 pub async fn open(params: fns::setTdlibParameters) -> Result<Self> {
79 let (connection, updates) = Connection::create();
80 let (buffered, closed) = Default::default();
81 let session = Self { connection, updates, buffered, closed };
82 if let Err(error) = session.connection.request(¶ms).await {
83 // Preserve the initiating failure after attempting native cleanup.
84 let _ = session.close().await;
85 return Err(error);
86 }
87 Ok(session)
88 }
89
90 /// Creates a session and waits for bot authorization to become ready.
91 ///
92 /// Submits the token when `TDLib` requests authentication. A session that is
93 /// already ready is reused without verifying it against `token`; use a fresh
94 /// directory or explicitly log out when switching accounts.
95 ///
96 /// # Errors
97 ///
98 /// Returns construction or token-request errors, or [`Error::Auth`] for an
99 /// authorization state this narrow helper does not handle. It attempts graceful
100 /// closure on returned authorization failure, preserving the original error.
101 /// For custom flows use [`open`](Self::open) and [`recv_auth`](Self::recv_auth).
102 ///
103 /// # Cancellation
104 ///
105 /// Dropping the future abandons construction/authentication and graceful cleanup.
106 pub async fn bot(params: fns::setTdlibParameters, token: &str) -> Result<Self> {
107 let mut session = Self::open(params).await?;
108 if let Err(error) = session.authorize_bot(token).await {
109 let _ = session.close().await;
110 return Err(error);
111 }
112 Ok(session)
113 }
114
115 /// Returns a detached request client for this session.
116 ///
117 /// The returned [`Client`] holds no borrow of the owner, so it can be used
118 /// concurrently with [`recv`](Self::recv). Cloning it does not create another
119 /// native client or extend the owner's operational lifetime.
120 pub fn client(&self) -> Client {
121 Client(Arc::downgrade(&self.connection))
122 }
123
124 /// Returns the next non-authorization update, or `None` after closure.
125 ///
126 /// Updates buffered by [`recv_auth`](Self::recv_auth) are returned first,
127 /// in their original order. Authorization transitions are consumed internally
128 /// and never returned here; consume them through `recv_auth` when needed.
129 ///
130 /// Requests and message tracking progress without polling this method, but
131 /// the unbounded application queue continues to grow until it is drained.
132 ///
133 /// # Cancellation safety
134 ///
135 /// Cancelling a pending receive does not lose an application update. It may
136 /// already have consumed authorization transitions, which are excluded from
137 /// this API. Do not alternate this method with `recv_auth` expecting an auth
138 /// transition consumed here to be replayed there.
139 pub async fn recv(&mut self) -> Option<Update> {
140 loop {
141 let update = match self.buffered.pop_front() {
142 Some(update) => update,
143 None if self.closed => return None,
144 None => self.receive().await,
145 };
146 if let Update::updateAuthorizationState(_) = update {
147 continue;
148 }
149 return Some(update);
150 }
151 }
152
153 /// Returns the next authorization transition, buffering other updates.
154 ///
155 /// This is an event stream, not a query for the current authorization state.
156 /// Once closure has been observed, subsequent calls return
157 /// `authorizationStateClosed` immediately. Otherwise this can wait indefinitely,
158 /// including when the client is already ready and no new transition occurs.
159 ///
160 /// Buffered application updates remain available through [`recv`](Self::recv).
161 /// Cancelling a pending call preserves those buffered updates.
162 pub async fn recv_auth(&mut self) -> AuthorizationState {
163 if self.closed {
164 return AuthorizationState::authorizationStateClosed;
165 }
166 loop {
167 match self.receive().await {
168 Update::updateAuthorizationState(update) => return update.authorization_state,
169 update => self.buffered.push_back(update),
170 }
171 }
172 }
173
174 /// Consumes the session and attempts graceful native closure.
175 ///
176 /// Closes request admission, sends `TDLib`'s generated `close`, and waits for
177 /// `authorizationStateClosed`. It then releases routing and waits for the
178 /// receiver's safe idle/ownership transition when necessary. If closure was
179 /// already consumed, it does not submit another close request.
180 ///
181 /// Application updates remaining at closure are discarded with the owner.
182 /// Drain any updates your application needs before initiating closure.
183 ///
184 /// # Errors
185 ///
186 /// Returns a close-request error while still clearing local waiters and
187 /// unregistering the client. An error is not proof that native shutdown finished.
188 /// Requests racing with closure may complete, receive a `TDLib` error, or
189 /// become [`Error::Disconnected`]; graceful close is not an application-task join.
190 ///
191 /// # Cancellation
192 ///
193 /// This operation has no built-in deadline and is not cancellation-safe.
194 /// Dropping its future can interrupt close or unregistration. Keep it driven
195 /// until it returns; ordinary `Drop` does not finish the protocol.
196 pub async fn close(mut self) -> Result {
197 let result = self.finish().await;
198 self.connection.disconnect();
199 runtime::unregister(self.connection.id).await;
200 result
201 }
202
203 async fn finish(&mut self) -> Result {
204 if self.closed {
205 return Ok(());
206 }
207 self.connection.close().await?;
208 while !self.closed {
209 self.receive().await;
210 }
211 Ok(())
212 }
213
214 async fn authorize_bot(&mut self, token: &str) -> Result {
215 loop {
216 match self.recv_auth().await {
217 AuthorizationState::authorizationStateReady => return Ok(()),
218 AuthorizationState::authorizationStateWaitTdlibParameters => {}
219 AuthorizationState::authorizationStateWaitPhoneNumber => {
220 self.connection.request(&fns::checkAuthenticationBotToken { token: token.into() }).await?;
221 }
222 state => return Err(Error::Auth(state)),
223 }
224 }
225 }
226
227 async fn receive(&mut self) -> Update {
228 // The owner's Arc keeps the channel sender alive even after native closure.
229 // Closed is an ordered authorization event, not channel EOF.
230 let update = self.updates.recv().await.expect("Connection owns the update sender");
231 if let Update::updateAuthorizationState(update) = &update
232 && let AuthorizationState::authorizationStateClosed = update.authorization_state
233 {
234 self.closed = true;
235 }
236 update
237 }
238}
239
240/// Builds editable `TDLib` parameters with local database and file directories.
241///
242/// Uses `directory/db` and `directory/files`; this function does not create them.
243/// Enables file, chat-info, and message databases. Language defaults to `en`,
244/// device model to `Server`, and application version to this crate's version.
245/// Other fields retain their generated defaults, including the encryption key.
246///
247/// These are conveniences, not validated configuration or a security policy.
248/// Disable databases you do not need and set application metadata/encryption
249/// explicitly where appropriate. Paths are converted with lossy UTF-8 conversion.
250///
251/// # Examples
252///
253/// ```
254/// let mut params = td_client::parameters(12345, "api hash", "session");
255/// params.use_message_database = false;
256/// params.device_model = "My application".into();
257/// assert!(!params.use_message_database);
258/// ```
259pub fn parameters(api_id: i32, api_hash: impl Into<String>, directory: impl AsRef<Path>) -> fns::setTdlibParameters {
260 let directory = directory.as_ref();
261 fns::setTdlibParameters {
262 api_id,
263 api_hash: api_hash.into(),
264 database_directory: directory.join("db").to_string_lossy().into_owned(),
265 files_directory: directory.join("files").to_string_lossy().into_owned(),
266 use_file_database: true,
267 use_chat_info_database: true,
268 use_message_database: true,
269 system_language_code: "en".into(),
270 device_model: "Server".into(),
271 application_version: env!("CARGO_PKG_VERSION").into(),
272 ..Default::default()
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use std::assert_matches;
279 use std::sync::Weak;
280 use std::time::Duration;
281
282 use td_types::fns;
283 use tokio::time::timeout;
284
285 use super::*;
286
287 #[tokio::test]
288 async fn authentication_buffers_application_updates_in_order() {
289 let (connection, updates) = Connection::fixture();
290 let mut session = Session { connection: Arc::clone(&connection), updates, buffered: VecDeque::new(), closed: false };
291 connection.update(br#"{"@type":"updateOption","name":"first","value":{"@type":"optionValueEmpty"}}"#);
292 connection.update(br#"{"@type":"updateOption","name":"second","value":{"@type":"optionValueEmpty"}}"#);
293 let auth = br#"{"@type":"updateAuthorizationState","authorization_state":{"@type":"authorizationStateWaitPhoneNumber"}}"#;
294 connection.update(auth);
295 connection.update(br#"{"@type":"updateOption","name":"third","value":{"@type":"optionValueEmpty"}}"#);
296 let exercise = async {
297 let authorization = session.recv_auth().await;
298 assert_matches!(authorization, AuthorizationState::authorizationStateWaitPhoneNumber);
299 for expected in ["first", "second", "third"] {
300 let update = session.recv().await;
301 assert_matches!(update, Some(Update::updateOption(option)) if option.name == expected);
302 }
303 };
304 timeout(Duration::from_secs(1), exercise).await.unwrap();
305 }
306
307 fn assert_send(_: impl Send) {}
308
309 #[test]
310 fn generic_operations_with_borrowed_callbacks_are_send() {
311 let client = Client(Weak::new());
312 let mut observe = |_, _| {};
313 let request = fns::sendBotStartMessage::default();
314 assert_send(client.track(&request, None, Some(&mut observe)));
315 let request = fns::forwardMessages::default();
316 assert_send(client.track_all(&request, None, Some(&mut observe)));
317 let request = fns::downloadFile { synchronous: true, ..Default::default() };
318 assert_send(client.download(&request, None, Some(&mut observe)));
319 }
320}