Skip to main content

hermes_tdata/
tdesktop.rs

1//! `TDesktop` client implementation
2//!
3//! Main entry point for parsing tdata folders.
4
5use std::path::{Path, PathBuf};
6
7use crate::account::Account;
8use crate::crypto::AuthKey;
9use crate::storage::{
10    KeyInfo, decrypt_key_data, get_absolute_path, get_default_tdata_path, read_key_data,
11    read_mtp_data,
12};
13use crate::{DEFAULT_KEY_FILE, Error, Result};
14
15/// Telegram Desktop client representation
16///
17/// Represents a parsed tdata folder with all its accounts.
18#[derive(Debug)]
19pub struct TDesktop {
20    /// Base path to the tdata folder
21    base_path: PathBuf,
22    /// Key file name (usually "data")
23    key_file: String,
24    /// Passcode used for decryption (empty if no passcode)
25    passcode: String,
26    /// Local encryption key
27    local_key: AuthKey,
28    /// List of accounts
29    accounts: Vec<Account>,
30    /// App version from tdata
31    app_version: u32,
32}
33
34impl TDesktop {
35    /// Load `TDesktop` from the default tdata location
36    ///
37    /// # Returns
38    /// - `Ok(TDesktop)` if loading succeeded
39    /// - `Err(Error::FolderNotFound)` if the default location doesn't exist
40    pub fn from_default() -> Result<Self> {
41        let path = get_default_tdata_path()
42            .ok_or_else(|| Error::FolderNotFound { path: PathBuf::from("(default tdata path)") })?;
43
44        Self::from_path(path)
45    }
46
47    /// Load `TDesktop` from a specific path
48    ///
49    /// # Arguments
50    /// - `path`: Path to the tdata folder
51    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
52        Self::with_options(path, None, None)
53    }
54
55    /// Load `TDesktop` with a passcode
56    ///
57    /// Use this when the tdata is protected with a Local Passcode.
58    ///
59    /// # Arguments
60    /// - `path`: Path to the tdata folder
61    /// - `passcode`: The Local Passcode
62    pub fn from_path_with_passcode<P: AsRef<Path>>(path: P, passcode: &str) -> Result<Self> {
63        Self::with_options(path, Some(passcode), None)
64    }
65
66    /// Load `TDesktop` with all options
67    ///
68    /// # Arguments
69    /// - `path`: Path to the tdata folder
70    /// - `passcode`: Optional Local Passcode
71    /// - `key_file`: Optional key file name (default: "data")
72    pub fn with_options<P: AsRef<Path>>(
73        path: P,
74        passcode: Option<&str>,
75        key_file: Option<&str>,
76    ) -> Result<Self> {
77        let base_path = get_absolute_path(path.as_ref().to_str().unwrap_or(""));
78
79        if !base_path.exists() {
80            return Err(Error::FolderNotFound { path: base_path });
81        }
82
83        let key_file = key_file.unwrap_or(DEFAULT_KEY_FILE).to_owned();
84        let passcode = passcode.unwrap_or("").to_owned();
85
86        // Read and decrypt key data
87        let key_data = read_key_data(&base_path, &key_file)?;
88
89        let KeyInfo { local_key, account_indices } =
90            decrypt_key_data(&key_data, passcode.as_bytes())?;
91
92        tracing::info!("Loaded key data: {} accounts found", account_indices.len());
93
94        // Load accounts
95        let mut accounts = Vec::new();
96        for index in account_indices {
97            match Self::load_account(&base_path, index, &local_key, &key_file) {
98                Ok(account) => {
99                    tracing::info!(
100                        "Loaded account {}: dc_id={}, user_id={}",
101                        index,
102                        account.dc_id(),
103                        account.user_id()
104                    );
105                    accounts.push(account);
106                },
107                Err(e) => {
108                    tracing::warn!("Failed to load account {}: {}", index, e);
109                },
110            }
111        }
112
113        if accounts.is_empty() {
114            return Err(Error::NoAccounts);
115        }
116
117        Ok(Self {
118            base_path,
119            key_file,
120            passcode,
121            local_key,
122            accounts,
123            app_version: key_data.version,
124        })
125    }
126
127    /// Load a single account
128    fn load_account(
129        base_path: &Path,
130        index: i32,
131        local_key: &AuthKey,
132        key_file: &str,
133    ) -> Result<Account> {
134        let mtp_data = read_mtp_data(base_path, index, local_key, key_file)?;
135
136        Ok(Account::new(index, mtp_data.dc_id, mtp_data.user_id, mtp_data.auth_key))
137    }
138
139    /// Get the base path to the tdata folder
140    #[must_use]
141    pub fn base_path(&self) -> &Path {
142        &self.base_path
143    }
144
145    /// Get the number of accounts
146    #[must_use]
147    pub const fn accounts_count(&self) -> usize {
148        self.accounts.len()
149    }
150
151    /// Get all accounts
152    #[must_use]
153    pub fn accounts(&self) -> &[Account] {
154        &self.accounts
155    }
156
157    /// Get the main (first) account
158    #[must_use]
159    pub fn main_account(&self) -> Option<&Account> {
160        self.accounts.first()
161    }
162
163    /// Get an account by index
164    #[must_use]
165    pub fn account(&self, index: usize) -> Option<&Account> {
166        self.accounts.get(index)
167    }
168
169    /// Get the app version
170    #[must_use]
171    pub const fn app_version(&self) -> u32 {
172        self.app_version
173    }
174
175    /// Check if the tdata has a passcode
176    #[must_use]
177    pub const fn has_passcode(&self) -> bool {
178        !self.passcode.is_empty()
179    }
180
181    /// Get the key file name
182    #[must_use]
183    pub fn key_file(&self) -> &str {
184        &self.key_file
185    }
186
187    /// Get the local encryption key
188    #[must_use]
189    pub const fn local_key(&self) -> &AuthKey {
190        &self.local_key
191    }
192}
193
194/// Builder for `TDesktop` with more control over loading
195#[derive(Debug)]
196pub struct TDesktopBuilder {
197    path: PathBuf,
198    passcode: Option<String>,
199    key_file: Option<String>,
200}
201
202impl TDesktopBuilder {
203    /// Create a new builder with the given path
204    pub fn new<P: AsRef<Path>>(path: P) -> Self {
205        Self { path: path.as_ref().to_path_buf(), passcode: None, key_file: None }
206    }
207
208    /// Set the passcode
209    pub fn passcode(mut self, passcode: impl Into<String>) -> Self {
210        self.passcode = Some(passcode.into());
211        self
212    }
213
214    /// Set the key file name
215    pub fn key_file(mut self, key_file: impl Into<String>) -> Self {
216        self.key_file = Some(key_file.into());
217        self
218    }
219
220    /// Build and load the `TDesktop`
221    pub fn build(self) -> Result<TDesktop> {
222        TDesktop::with_options(self.path, self.passcode.as_deref(), self.key_file.as_deref())
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_builder() {
232        let builder = TDesktopBuilder::new("/path/to/tdata").passcode("secret").key_file("custom");
233
234        assert_eq!(builder.path, PathBuf::from("/path/to/tdata"));
235        assert_eq!(builder.passcode, Some("secret".to_string()));
236        assert_eq!(builder.key_file, Some("custom".to_string()));
237    }
238}