1use 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#[derive(Debug)]
19pub struct TDesktop {
20 base_path: PathBuf,
22 key_file: String,
24 passcode: String,
26 local_key: AuthKey,
28 accounts: Vec<Account>,
30 app_version: u32,
32}
33
34impl TDesktop {
35 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 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
52 Self::with_options(path, None, None)
53 }
54
55 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 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 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 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 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 #[must_use]
141 pub fn base_path(&self) -> &Path {
142 &self.base_path
143 }
144
145 #[must_use]
147 pub const fn accounts_count(&self) -> usize {
148 self.accounts.len()
149 }
150
151 #[must_use]
153 pub fn accounts(&self) -> &[Account] {
154 &self.accounts
155 }
156
157 #[must_use]
159 pub fn main_account(&self) -> Option<&Account> {
160 self.accounts.first()
161 }
162
163 #[must_use]
165 pub fn account(&self, index: usize) -> Option<&Account> {
166 self.accounts.get(index)
167 }
168
169 #[must_use]
171 pub const fn app_version(&self) -> u32 {
172 self.app_version
173 }
174
175 #[must_use]
177 pub const fn has_passcode(&self) -> bool {
178 !self.passcode.is_empty()
179 }
180
181 #[must_use]
183 pub fn key_file(&self) -> &str {
184 &self.key_file
185 }
186
187 #[must_use]
189 pub const fn local_key(&self) -> &AuthKey {
190 &self.local_key
191 }
192}
193
194#[derive(Debug)]
196pub struct TDesktopBuilder {
197 path: PathBuf,
198 passcode: Option<String>,
199 key_file: Option<String>,
200}
201
202impl TDesktopBuilder {
203 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 pub fn passcode(mut self, passcode: impl Into<String>) -> Self {
210 self.passcode = Some(passcode.into());
211 self
212 }
213
214 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 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}