Skip to main content

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(&params).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}