| use crate::models::{Account, TokenData}; |
| use crate::modules::{account, db}; |
| use crate::utils::protobuf; |
| use base64::{engine::general_purpose, Engine as _}; |
| use serde_json::Value; |
| use std::fs; |
| use std::path::PathBuf; |
|
|
| #[derive(Debug, Clone)] |
| struct ImportedOAuthState { |
| refresh_token: String, |
| is_gcp_tos: bool, |
| project_id: Option<String>, |
| } |
|
|
| |
| pub async fn import_from_v1() -> Result<Vec<Account>, String> { |
| use crate::modules::oauth; |
|
|
| let home = dirs::home_dir().ok_or("Failed to get home directory")?; |
| |
| |
| let v1_dir = home.join(".antigravity-agent"); |
| |
| let mut imported_accounts = Vec::new(); |
| |
| |
| let index_files = vec![ |
| "antigravity_accounts.json", |
| "accounts.json", |
| ]; |
| |
| let mut found_index = false; |
|
|
| for index_filename in index_files { |
| let v1_accounts_path = v1_dir.join(index_filename); |
| |
| if !v1_accounts_path.exists() { |
| continue; |
| } |
| |
| found_index = true; |
| crate::modules::logger::log_info(&format!("V1 data discovered: {:?}", v1_accounts_path)); |
| |
| let content = match fs::read_to_string(&v1_accounts_path) { |
| Ok(c) => c, |
| Err(e) => { |
| crate::modules::logger::log_warn(&format!("Failed to read index: {}", e)); |
| continue; |
| } |
| }; |
| |
| let v1_index: Value = match serde_json::from_str(&content) { |
| Ok(v) => v, |
| Err(e) => { |
| crate::modules::logger::log_warn(&format!("Failed to parse index JSON: {}", e)); |
| continue; |
| } |
| }; |
| |
| |
| let accounts_map = if let Some(map) = v1_index.as_object() { |
| if let Some(accounts) = map.get("accounts").and_then(|v| v.as_object()) { |
| accounts |
| } else { |
| map |
| } |
| } else { |
| continue; |
| }; |
| |
| for (id, acc_info) in accounts_map { |
| let email_placeholder = acc_info.get("email").and_then(|v| v.as_str()).unwrap_or("Unknown").to_string(); |
| |
| |
| if !acc_info.is_object() { |
| continue; |
| } |
| |
| let backup_file_str = acc_info.get("backup_file").and_then(|v| v.as_str()); |
| let data_file_str = acc_info.get("data_file").and_then(|v| v.as_str()); |
| |
| |
| let target_file = backup_file_str.or(data_file_str); |
| |
| if target_file.is_none() { |
| crate::modules::logger::log_warn(&format!("Account {} ({}) missing data file path", id, email_placeholder)); |
| continue; |
| } |
| |
| let mut backup_path = PathBuf::from(target_file.unwrap()); |
| |
| |
| if !backup_path.exists() { |
| backup_path = v1_dir.join(backup_path.file_name().unwrap_or_default()); |
| } |
| |
| |
| if !backup_path.exists() { |
| let file_name = backup_path.file_name().unwrap_or_default(); |
| let try_backups = v1_dir.join("backups").join(file_name); |
| if try_backups.exists() { |
| backup_path = try_backups; |
| } else { |
| let try_accounts = v1_dir.join("accounts").join(file_name); |
| if try_accounts.exists() { |
| backup_path = try_accounts; |
| } |
| } |
| } |
| |
| if !backup_path.exists() { |
| crate::modules::logger::log_warn(&format!("Account {} ({}) backup file not found: {:?}", id, email_placeholder, backup_path)); |
| continue; |
| } |
| |
| |
| if let Ok(backup_content) = fs::read_to_string(&backup_path) { |
| if let Ok(backup_json) = serde_json::from_str::<Value>(&backup_content) { |
| |
| |
| |
| |
| |
| let mut refresh_token_opt = None; |
| |
| |
| if let Some(token_data) = backup_json.get("token") { |
| if let Some(rt) = token_data.get("refresh_token").and_then(|v| v.as_str()) { |
| refresh_token_opt = Some(rt.to_string()); |
| } |
| } |
| |
| |
| if refresh_token_opt.is_none() { |
| if let Some(state_b64) = backup_json.get("jetskiStateSync.agentManagerInitState").and_then(|v| v.as_str()) { |
| |
| if let Ok(blob) = general_purpose::STANDARD.decode(state_b64) { |
| if let Ok(Some(oauth_data)) = protobuf::find_field(&blob, 6) { |
| if let Ok(Some(refresh_bytes)) = protobuf::find_field(&oauth_data, 3) { |
| if let Ok(rt) = String::from_utf8(refresh_bytes) { |
| refresh_token_opt = Some(rt); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| if let Some(refresh_token) = refresh_token_opt { |
| crate::modules::logger::log_info(&format!( |
| "Importing account: {}", |
| email_placeholder |
| )); |
| let (email, access_token, expires_in, oauth_client_key) = |
| match oauth::refresh_access_token(&refresh_token, None).await { |
| Ok(token_resp) => { |
| let oauth_client_key = token_resp.oauth_client_key.clone(); |
| match oauth::get_user_info(&token_resp.access_token, None).await |
| { |
| Ok(user_info) => ( |
| user_info.email, |
| token_resp.access_token, |
| token_resp.expires_in, |
| oauth_client_key, |
| ), |
| Err(_) => ( |
| email_placeholder.clone(), |
| token_resp.access_token, |
| token_resp.expires_in, |
| oauth_client_key, |
| ), |
| } |
| } |
| Err(e) => { |
| crate::modules::logger::log_warn(&format!( |
| "Token refresh failed (likely expired): {}", |
| e |
| )); |
| ( |
| email_placeholder.clone(), |
| "imported_access_token".to_string(), |
| 0, |
| None, |
| ) |
| } |
| }; |
| let token_data = TokenData::new( |
| access_token, |
| refresh_token, |
| expires_in, |
| Some(email.clone()), |
| None, |
| None, |
| true, |
| ) |
| .with_oauth_client_key(oauth_client_key); |
| |
| match account::upsert_account(email.clone(), None, token_data) { |
| Ok(acc) => { |
| crate::modules::logger::log_info(&format!( |
| "Import successful: {}", |
| email |
| )); |
| imported_accounts.push(acc); |
| } |
| Err(e) => crate::modules::logger::log_error(&format!( |
| "Import save failed {}: {}", |
| email, e |
| )), |
| } |
| } else { |
| crate::modules::logger::log_warn(&format!( |
| "Account {} data file missing Refresh Token", |
| email_placeholder |
| )); |
| } |
| } |
| } |
| } |
| } |
| |
| if !found_index { |
| return Err("V1 account data file not found".to_string()); |
| } |
| |
| Ok(imported_accounts) |
| } |
|
|
| |
| pub async fn import_from_custom_db_path(path_str: String) -> Result<Account, String> { |
| use crate::modules::oauth; |
|
|
| let path = PathBuf::from(path_str); |
| if !path.exists() { |
| return Err(format!("File does not exist: {:?}", path)); |
| } |
|
|
| let oauth_state = extract_oauth_state_from_file(&path)?; |
| let refresh_token = oauth_state.refresh_token.clone(); |
| |
| |
| crate::modules::logger::log_info("Getting user info using Refresh Token..."); |
| let token_resp = oauth::refresh_access_token(&refresh_token, None).await?; |
| let user_info = oauth::get_user_info(&token_resp.access_token, None).await?; |
| |
| let email = user_info.email; |
| |
| crate::modules::logger::log_info(&format!("Successfully retrieved account info: {}", email)); |
| |
| let token_data = TokenData::new( |
| token_resp.access_token, |
| refresh_token, |
| token_resp.expires_in, |
| Some(email.clone()), |
| oauth_state.project_id, |
| None, |
| oauth_state.is_gcp_tos, |
| ) |
| .with_oauth_client_key(token_resp.oauth_client_key); |
| |
| account::upsert_account(email.clone(), user_info.name, token_data) |
| } |
|
|
| |
| pub async fn import_from_db() -> Result<Account, String> { |
| let db_path = db::get_db_path()?; |
| import_from_custom_db_path(db_path.to_string_lossy().to_string()).await |
| } |
|
|
| |
| pub fn extract_refresh_token_from_file(db_path: &PathBuf) -> Result<String, String> { |
| extract_oauth_state_from_file(db_path).map(|state| state.refresh_token) |
| } |
|
|
| fn extract_enterprise_project_id_from_conn( |
| conn: &rusqlite::Connection, |
| ) -> Result<Option<String>, String> { |
| let entry_b64: Option<String> = conn |
| .query_row( |
| "SELECT value FROM ItemTable WHERE key = ?", |
| ["antigravityUnifiedStateSync.enterprisePreferences"], |
| |row| row.get(0), |
| ) |
| .ok(); |
|
|
| let Some(entry_b64) = entry_b64 else { |
| return Ok(None); |
| }; |
|
|
| let (sentinel_key, payload) = protobuf::decode_unified_state_entry(&entry_b64)?; |
| if sentinel_key != "enterpriseGcpProjectId" { |
| return Ok(None); |
| } |
|
|
| let Some(project_bytes) = protobuf::find_field(&payload, 3)? else { |
| return Ok(None); |
| }; |
|
|
| let project_id = String::from_utf8(project_bytes) |
| .map_err(|_| "enterpriseGcpProjectId is not UTF-8 encoded".to_string())?; |
| if project_id.trim().is_empty() { |
| Ok(None) |
| } else { |
| Ok(Some(project_id)) |
| } |
| } |
|
|
| fn extract_oauth_state_from_file(db_path: &PathBuf) -> Result<ImportedOAuthState, String> { |
| use base64::{engine::general_purpose, Engine as _}; |
| |
| if !db_path.exists() { |
| return Err(format!("Database file not found: {:?}", db_path)); |
| } |
| |
| |
| let conn = rusqlite::Connection::open(db_path) |
| .map_err(|e| format!("Failed to open database: {}", e))?; |
| |
| |
| |
| |
| let new_format_data: Option<String> = conn |
| .query_row( |
| "SELECT value FROM ItemTable WHERE key = ?", |
| ["antigravityUnifiedStateSync.oauthToken"], |
| |row| row.get(0), |
| ) |
| .ok(); |
|
|
| if let Some(outer_b64) = new_format_data { |
| crate::modules::logger::log_info( |
| "Detected new format database (antigravityUnifiedStateSync.oauthToken)", |
| ); |
| let (sentinel_key, oauth_info_blob) = protobuf::decode_unified_state_entry(&outer_b64)?; |
| if sentinel_key != "oauthTokenInfoSentinelKey" { |
| return Err(format!("Unexpected OAuth sentinel key: {}", sentinel_key)); |
| } |
| |
| |
| let refresh_bytes = protobuf::find_field(&oauth_info_blob, 3) |
| .map_err(|e| format!("Parsing OAuthInfo Field 3 failed: {}", e))? |
| .ok_or("Refresh Token not found in OAuthInfo (Field 3)")?; |
| |
| let refresh_token = String::from_utf8(refresh_bytes) |
| .map_err(|_| "Refresh Token is not UTF-8 encoded".to_string())?; |
| let is_gcp_tos = protobuf::find_varint_field(&oauth_info_blob, 6)?.unwrap_or(1) != 0; |
| let project_id = extract_enterprise_project_id_from_conn(&conn)?; |
|
|
| return Ok(ImportedOAuthState { |
| refresh_token, |
| is_gcp_tos, |
| project_id, |
| }); |
| } |
|
|
| |
| crate::modules::logger::log_info( |
| "Falling back to old format database (jetskiStateSync.agentManagerInitState)", |
| ); |
| let current_data: String = conn |
| .query_row( |
| "SELECT value FROM ItemTable WHERE key = ?", |
| ["jetskiStateSync.agentManagerInitState"], |
| |row| row.get(0), |
| ) |
| .map_err(|_| "Login state data not found in either format".to_string())?; |
| |
| |
| let blob = general_purpose::STANDARD |
| .decode(¤t_data) |
| .map_err(|e| format!("Base64 decoding failed: {}", e))?; |
| |
| |
| let oauth_data = protobuf::find_field(&blob, 6) |
| .map_err(|e| format!("Protobuf parsing failed: {}", e))? |
| .ok_or("OAuth data not found (Field 6)")?; |
| |
| |
| let refresh_bytes = protobuf::find_field(&oauth_data, 3) |
| .map_err(|e| format!("OAuth data parsing failed: {}", e))? |
| .ok_or("Refresh Token not included in data (Field 3)")?; |
| |
| let refresh_token = String::from_utf8(refresh_bytes) |
| .map_err(|_| "Refresh Token is not UTF-8 encoded".to_string())?; |
|
|
| Ok(ImportedOAuthState { |
| refresh_token, |
| is_gcp_tos: true, |
| project_id: extract_enterprise_project_id_from_conn(&conn)?, |
| }) |
| } |
|
|
| |
| pub fn get_refresh_token_from_db() -> Result<String, String> { |
| let db_path = db::get_db_path()?; |
| extract_refresh_token_from_file(&db_path) |
| } |
|
|