1use std::fmt;
6use std::path::{Path, PathBuf};
7
8use crate::account::Account;
9use crate::crypto::AuthKey;
10use crate::storage::{
11 decrypt_key_data, get_absolute_path, get_default_tdata_path, read_key_data, read_mtp_data,
12 KeyInfo,
13};
14use crate::{Error, Result, DEFAULT_KEY_FILE};
15
16pub struct TDesktop {
20 base_path: PathBuf,
22 key_file: String,
24 has_passcode: bool,
28 local_key: AuthKey,
30 accounts: Vec<Account>,
32 app_version: u32,
34}
35
36impl fmt::Debug for TDesktop {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 f.debug_struct("TDesktop")
39 .field("base_path", &"<redacted>")
40 .field("key_file", &self.key_file)
41 .field("has_passcode", &self.has_passcode)
42 .field("accounts_count", &self.accounts.len())
43 .field("app_version", &self.app_version)
44 .finish()
45 }
46}
47
48impl TDesktop {
49 pub fn from_default() -> Result<Self> {
55 let path = get_default_tdata_path().ok_or_else(|| Error::FolderNotFound {
56 path: PathBuf::from("(default tdata path)"),
57 })?;
58
59 Self::from_path(path)
60 }
61
62 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
67 Self::with_options(path, None, None)
68 }
69
70 pub fn from_path_with_passcode<P: AsRef<Path>>(path: P, passcode: &str) -> Result<Self> {
78 Self::with_options(path, Some(passcode), None)
79 }
80
81 pub fn with_options<P: AsRef<Path>>(
88 path: P,
89 passcode: Option<&str>,
90 key_file: Option<&str>,
91 ) -> Result<Self> {
92 let base_path = get_absolute_path(path.as_ref());
93
94 if !base_path.exists() {
95 return Err(Error::FolderNotFound {
96 path: base_path.clone(),
97 });
98 }
99
100 let key_file = key_file.unwrap_or(DEFAULT_KEY_FILE).to_string();
101 let passcode = passcode.unwrap_or("");
102 let has_passcode = !passcode.is_empty();
103
104 let key_data = read_key_data(&base_path, &key_file)?;
106
107 let KeyInfo {
108 local_key,
109 account_indices,
110 } = decrypt_key_data(&key_data, passcode.as_bytes())?;
111
112 tracing::info!("Loaded key data: {} accounts found", account_indices.len());
113
114 let mut accounts = Vec::new();
116 for index in account_indices {
117 match Self::load_account(&base_path, index, &local_key, &key_file) {
118 Ok(account) => {
119 tracing::info!("Loaded account {} for DC {}", index, account.dc_id());
120 accounts.push(account);
121 }
122 Err(e) => {
123 tracing::warn!("Failed to load account {}: {}", index, e);
124 }
125 }
126 }
127
128 if accounts.is_empty() {
129 return Err(Error::NoAccounts);
130 }
131
132 Ok(Self {
133 base_path,
134 key_file,
135 has_passcode,
136 local_key,
137 accounts,
138 app_version: key_data.version,
139 })
140 }
141
142 fn load_account(
144 base_path: &Path,
145 index: i32,
146 local_key: &AuthKey,
147 key_file: &str,
148 ) -> Result<Account> {
149 let mtp_data = read_mtp_data(base_path, index, local_key, key_file)?;
150
151 Ok(Account::new(
152 index,
153 mtp_data.dc_id,
154 mtp_data.user_id,
155 mtp_data.auth_key,
156 ))
157 }
158
159 pub fn base_path(&self) -> &Path {
163 &self.base_path
164 }
165
166 pub fn accounts_count(&self) -> usize {
168 self.accounts.len()
169 }
170
171 pub fn accounts(&self) -> &[Account] {
173 &self.accounts
174 }
175
176 pub fn main_account(&self) -> Option<&Account> {
178 self.accounts.first()
179 }
180
181 pub fn account(&self, index: usize) -> Option<&Account> {
183 self.accounts.get(index)
184 }
185
186 pub fn app_version(&self) -> u32 {
188 self.app_version
189 }
190
191 pub fn has_passcode(&self) -> bool {
193 self.has_passcode
194 }
195
196 pub fn key_file(&self) -> &str {
198 &self.key_file
199 }
200
201 pub fn local_key(&self) -> &AuthKey {
205 &self.local_key
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use std::path::PathBuf;
213
214 struct TDesktopBuilder {
216 path: PathBuf,
217 passcode: Option<String>,
218 key_file: Option<String>,
219 }
220
221 impl TDesktopBuilder {
222 fn new<P: AsRef<Path>>(path: P) -> Self {
224 Self {
225 path: path.as_ref().to_path_buf(),
226 passcode: None,
227 key_file: None,
228 }
229 }
230
231 fn passcode(mut self, passcode: impl Into<String>) -> Self {
233 self.passcode = Some(passcode.into());
234 self
235 }
236
237 fn key_file(mut self, key_file: impl Into<String>) -> Self {
239 self.key_file = Some(key_file.into());
240 self
241 }
242 }
243
244 #[test]
245 fn test_builder() {
246 let builder = TDesktopBuilder::new("/path/to/tdata")
247 .passcode("secret")
248 .key_file("custom");
249
250 assert_eq!(builder.path, PathBuf::from("/path/to/tdata"));
251 assert_eq!(builder.passcode, Some("secret".to_string()));
252 assert_eq!(builder.key_file, Some("custom".to_string()));
253 }
254
255 #[test]
256 fn debug_redacts_storage_path_and_account_identity() -> Result<()> {
257 let tdesktop = TDesktop {
258 base_path: PathBuf::from("/private/user/TelegramDesktop/tdata"),
259 key_file: "data".to_string(),
260 has_passcode: true,
261 local_key: AuthKey::from_bytes(&[0xCD; crate::AUTH_KEY_SIZE])?,
262 accounts: vec![Account::new(0, 2, 12_345_678, [0xAB; crate::AUTH_KEY_SIZE])],
263 app_version: 6_004_001,
264 };
265 let debug = format!("{tdesktop:?}");
266
267 assert!(debug.contains("<redacted>"));
268 assert!(!debug.contains("/private/user"));
269 assert!(!debug.contains("12345678"));
270 assert!(!debug.contains("171, 171"));
271 assert!(!debug.contains("205, 205"));
272 Ok(())
273 }
274}