use std::path::{Path, PathBuf};
use crate::account::Account;
use crate::crypto::AuthKey;
use crate::storage::{
KeyInfo, decrypt_key_data, get_absolute_path, get_default_tdata_path, read_key_data,
read_mtp_data,
};
use crate::{DEFAULT_KEY_FILE, Error, Result};
#[derive(Debug)]
pub struct TDesktop {
base_path: PathBuf,
key_file: String,
passcode: String,
local_key: AuthKey,
accounts: Vec<Account>,
app_version: u32,
}
impl TDesktop {
pub fn from_default() -> Result<Self> {
let path = get_default_tdata_path()
.ok_or_else(|| Error::FolderNotFound { path: PathBuf::from("(default tdata path)") })?;
Self::from_path(path)
}
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::with_options(path, None, None)
}
pub fn from_path_with_passcode<P: AsRef<Path>>(path: P, passcode: &str) -> Result<Self> {
Self::with_options(path, Some(passcode), None)
}
pub fn with_options<P: AsRef<Path>>(
path: P,
passcode: Option<&str>,
key_file: Option<&str>,
) -> Result<Self> {
let base_path = get_absolute_path(path.as_ref().to_str().unwrap_or(""));
if !base_path.exists() {
return Err(Error::FolderNotFound { path: base_path });
}
let key_file = key_file.unwrap_or(DEFAULT_KEY_FILE).to_owned();
let passcode = passcode.unwrap_or("").to_owned();
let key_data = read_key_data(&base_path, &key_file)?;
let KeyInfo { local_key, account_indices } =
decrypt_key_data(&key_data, passcode.as_bytes())?;
tracing::info!("Loaded key data: {} accounts found", account_indices.len());
let mut accounts = Vec::new();
for index in account_indices {
match Self::load_account(&base_path, index, &local_key, &key_file) {
Ok(account) => {
tracing::info!(
"Loaded account {}: dc_id={}, user_id={}",
index,
account.dc_id(),
account.user_id()
);
accounts.push(account);
},
Err(e) => {
tracing::warn!("Failed to load account {}: {}", index, e);
},
}
}
if accounts.is_empty() {
return Err(Error::NoAccounts);
}
Ok(Self {
base_path,
key_file,
passcode,
local_key,
accounts,
app_version: key_data.version,
})
}
fn load_account(
base_path: &Path,
index: i32,
local_key: &AuthKey,
key_file: &str,
) -> Result<Account> {
let mtp_data = read_mtp_data(base_path, index, local_key, key_file)?;
Ok(Account::new(index, mtp_data.dc_id, mtp_data.user_id, mtp_data.auth_key))
}
#[must_use]
pub fn base_path(&self) -> &Path {
&self.base_path
}
#[must_use]
pub const fn accounts_count(&self) -> usize {
self.accounts.len()
}
#[must_use]
pub fn accounts(&self) -> &[Account] {
&self.accounts
}
#[must_use]
pub fn main_account(&self) -> Option<&Account> {
self.accounts.first()
}
#[must_use]
pub fn account(&self, index: usize) -> Option<&Account> {
self.accounts.get(index)
}
#[must_use]
pub const fn app_version(&self) -> u32 {
self.app_version
}
#[must_use]
pub const fn has_passcode(&self) -> bool {
!self.passcode.is_empty()
}
#[must_use]
pub fn key_file(&self) -> &str {
&self.key_file
}
#[must_use]
pub const fn local_key(&self) -> &AuthKey {
&self.local_key
}
}
#[derive(Debug)]
pub struct TDesktopBuilder {
path: PathBuf,
passcode: Option<String>,
key_file: Option<String>,
}
impl TDesktopBuilder {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Self { path: path.as_ref().to_path_buf(), passcode: None, key_file: None }
}
pub fn passcode(mut self, passcode: impl Into<String>) -> Self {
self.passcode = Some(passcode.into());
self
}
pub fn key_file(mut self, key_file: impl Into<String>) -> Self {
self.key_file = Some(key_file.into());
self
}
pub fn build(self) -> Result<TDesktop> {
TDesktop::with_options(self.path, self.passcode.as_deref(), self.key_file.as_deref())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder() {
let builder = TDesktopBuilder::new("/path/to/tdata").passcode("secret").key_file("custom");
assert_eq!(builder.path, PathBuf::from("/path/to/tdata"));
assert_eq!(builder.passcode, Some("secret".to_string()));
assert_eq!(builder.key_file, Some("custom".to_string()));
}
}