pub mod database_studio; pub mod http_common; pub mod log_manager; pub mod store; pub mod vrchat_api; pub mod vrchat_status; pub mod websocket; use database_studio::{ColumnInfo, DatabaseStudio, QueryResult, TableInfo}; use log::info; use log_manager::{LogEntry, LogManager}; use std::sync::Arc; use store::{AccountStore, AppSettings, ImageCacheStore, SettingsStore, StoredAccount, UserStore}; use tauri::{Manager, State}; use tauri_specta::{Builder as SpectaBuilder, collect_commands}; use tokio::sync::Mutex; use vrchat_api::{ AgeVerificationStatus, AvatarPerformance, AvatarStyles, Badge, DeveloperType, DiscordDetails, FriendRequestStatus, GoogleDetails, LimitedAvatar, LimitedUserFriend, LimitedWorld, LoginCredentials, LoginResult, OrderOption, PastDisplayName, PerformanceRatings, ReleaseStatus, SteamDetails, TwoFactorMethod, UnityPackageSummary, UpdateStatusRequest, User, UserState, UserStatus, VRCError, VRChatClient, }; use vrchat_status::{StatusPage, SystemStatus, VRChatStatusResponse}; use websocket::VRChatWebSocket; #[cfg(debug_assertions)] use specta_typescript::Typescript; // Application State struct AppState { vrchat_client: Arc>, account_store: AccountStore, websocket: Arc>, user_store: UserStore, settings_store: SettingsStore, #[allow(dead_code)] image_cache: Arc, } // VRChat API Commands #[tauri::command] #[specta::specta] async fn vrchat_login( email: String, password: String, state: State<'_, AppState>, ) -> Result { let credentials = LoginCredentials { email, password }; let client = state.vrchat_client.lock().await; client.login(&credentials).await } #[tauri::command] #[specta::specta] async fn vrchat_verify_2fa( code: String, method: String, state: State<'_, AppState>, ) -> Result { let two_fa_method = TwoFactorMethod::from_str(&method) .ok_or_else(|| VRCError::invalid_input(format!("Invalid 2FA method: {}", method)))?; let client = state.vrchat_client.lock().await; client.verify_two_factor(&code, two_fa_method).await } #[tauri::command] #[specta::specta] async fn vrchat_get_current_user(state: State<'_, AppState>) -> Result { if let Some(user) = state.user_store.get_current_user().await { return Ok(user); } let client = state.vrchat_client.lock().await; let user = client.get_current_user().await?; state.user_store.set_current_user(user.clone()).await; Ok(user) } #[tauri::command] #[specta::specta] async fn vrchat_update_status( status: UserStatus, status_description: String, state: State<'_, AppState>, ) -> Result { let request = UpdateStatusRequest { status, status_description, }; let client = state.vrchat_client.lock().await; let user = client.update_status(&request).await?; drop(client); state.user_store.set_current_user(user.clone()).await; Ok(user) } #[tauri::command] #[specta::specta] async fn vrchat_logout(state: State<'_, AppState>) -> Result<(), VRCError> { let websocket = state.websocket.lock().await; websocket.stop().await; drop(websocket); state.user_store.clear_all().await; let client = state.vrchat_client.lock().await; client.logout().await } #[tauri::command] #[specta::specta] async fn websocket_start(state: State<'_, AppState>) -> Result<(), VRCError> { let client = state.vrchat_client.lock().await; let (auth_cookie, two_factor_cookie) = client.export_cookies().await; drop(client); log::debug!( "WebSocket starting with cookies - auth: {:?}, 2fa: {:?}", auth_cookie .as_ref() .map(|c| format!("{}...", &c.chars().take(20).collect::())), two_factor_cookie .as_ref() .map(|c| format!("{}...", &c.chars().take(20).collect::())) ); let websocket = state.websocket.lock().await; websocket.set_cookies(auth_cookie, two_factor_cookie).await; websocket.start().await } #[tauri::command] #[specta::specta] async fn websocket_stop(state: State<'_, AppState>) -> Result<(), VRCError> { let websocket = state.websocket.lock().await; websocket.stop().await; Ok(()) } #[tauri::command] #[specta::specta] async fn vrchat_get_online_friends( state: State<'_, AppState>, ) -> Result, VRCError> { let cached_friends = state.user_store.get_all_friends().await; if !cached_friends.is_empty() { return Ok(cached_friends); } let client = state.vrchat_client.lock().await; let friends = client.get_all_friends().await?; state.user_store.set_friends(friends.clone()).await; Ok(friends) } #[tauri::command] #[specta::specta] async fn vrchat_get_uploaded_worlds( state: State<'_, AppState>, ) -> Result, VRCError> { let client = state.vrchat_client.lock().await; client.get_uploaded_worlds().await } #[tauri::command] #[specta::specta] async fn vrchat_get_uploaded_avatars( state: State<'_, AppState>, ) -> Result, VRCError> { let client = state.vrchat_client.lock().await; client.get_uploaded_avatars().await } #[tauri::command] #[specta::specta] async fn get_online_friends( state: State<'_, AppState>, ) -> Result, VRCError> { Ok(state.user_store.get_all_friends().await) } #[tauri::command] #[specta::specta] async fn check_image_cached( url: String, state: State<'_, AppState>, ) -> Result, VRCError> { if url.trim().is_empty() { return Ok(None); } let cache = state.image_cache.clone(); if let Some(path) = cache.get_cached_path(&url).await { Ok(Some(path.to_string_lossy().to_string())) } else { Ok(None) } } #[tauri::command] #[specta::specta] async fn cache_image(url: String, state: State<'_, AppState>) -> Result { if url.trim().is_empty() { return Err(VRCError::invalid_input("Image URL is empty".to_string())); } info!("Caching image: {}", url); let client = state.vrchat_client.lock().await; let (auth_cookie, two_factor_cookie) = client.export_cookies().await; drop(client); let cookies = match (auth_cookie, two_factor_cookie) { (Some(auth), Some(two_fa)) => Some(format!("{}; {}", auth, two_fa)), (Some(auth), None) => Some(auth), (None, Some(two_fa)) => Some(two_fa), (None, None) => None, }; let cache = state.image_cache.clone(); let path = cache .get_or_fetch(&url, cookies) .await .map_err(VRCError::unknown)?; info!("Image cached to: {}", path.display()); Ok(path.to_string_lossy().to_string()) } #[tauri::command] #[specta::specta] async fn get_cache_directory(state: State<'_, AppState>) -> Result { let cache = state.image_cache.clone(); let dir = cache.get_cache_dir(); Ok(dir.to_string_lossy().to_string()) } #[tauri::command] #[specta::specta] async fn get_all_friends(state: State<'_, AppState>) -> Result, VRCError> { Ok(state.user_store.get_all_friends().await) } #[tauri::command] #[specta::specta] async fn get_user( user_id: String, state: State<'_, AppState>, ) -> Result, VRCError> { Ok(state.user_store.get_user(&user_id).await) } #[tauri::command] #[specta::specta] async fn get_user_by_id(user_id: String, state: State<'_, AppState>) -> Result { if let Some(cached_user) = state.user_store.get_full_user(&user_id).await { log::debug!("get_user_by_id: Returning cached user for {}", user_id); return Ok(cached_user); } log::info!("get_user_by_id: Fetching user {} from API", user_id); let client = state.vrchat_client.lock().await; let user = client.get_user_by_id(&user_id).await?; drop(client); log::info!("get_user_by_id: Successfully fetched user {}", user_id); state.user_store.cache_full_user(user.clone()).await; Ok(user) } #[tauri::command] #[specta::specta] async fn is_friend(user_id: String, state: State<'_, AppState>) -> Result { Ok(state.user_store.is_friend(&user_id).await) } #[tauri::command] #[specta::specta] async fn is_user_online(user_id: String, state: State<'_, AppState>) -> Result { Ok(state.user_store.is_user_online(&user_id).await) } #[tauri::command] #[specta::specta] async fn vrchat_check_session(state: State<'_, AppState>) -> Result { let client = state.vrchat_client.lock().await; Ok(client.has_valid_session().await) } #[tauri::command] #[specta::specta] async fn vrchat_clear_session(state: State<'_, AppState>) -> Result<(), VRCError> { let client = state.vrchat_client.lock().await; client.clear_cookies().await; drop(client); state.user_store.clear_all().await; state .account_store .clear_last_active_account() .await .map_err(|e| VRCError::unknown(e))?; Ok(()) } // Account Management Commands #[tauri::command] #[specta::specta] async fn save_current_account(user: User, state: State<'_, AppState>) -> Result<(), VRCError> { let client = state.vrchat_client.lock().await; let (auth_cookie, two_factor_cookie) = client.export_cookies().await; let avatar_override = user .user_icon .clone() .or_else(|| user.profile_pic_override.clone()) .or_else(|| user.profile_pic_override_thumbnail.clone()); let avatar_fallback = user .current_avatar_thumbnail_image_url .clone() .or_else(|| user.current_avatar_image_url.clone()); let account = StoredAccount { user_id: user.id.clone(), username: user.username.clone(), display_name: user.display_name.clone(), avatar_url: avatar_override.clone().or_else(|| avatar_fallback.clone()), avatar_fallback_url: avatar_fallback, auth_cookie, two_factor_cookie, last_login: chrono::Utc::now().to_rfc3339(), }; state .account_store .save_account(account) .await .map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn get_all_accounts(state: State<'_, AppState>) -> Result, VRCError> { state .account_store .get_all_accounts() .await .map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn switch_account(user_id: String, state: State<'_, AppState>) -> Result { state.user_store.clear_all().await; let account = state .account_store .get_account(&user_id) .await .map_err(|e| VRCError::unknown(e))? .ok_or_else(|| VRCError::invalid_input("Account not found"))?; state .account_store .set_active_account(&user_id) .await .map_err(|e| VRCError::unknown(e))?; let client = state.vrchat_client.lock().await; client .import_cookies(account.auth_cookie, account.two_factor_cookie) .await; match client.get_current_user().await { Ok(user) => { state.user_store.set_current_user(user.clone()).await; Ok(user) } Err(err) => { // TODO: Handle if switching account fails Err(err) } } } #[tauri::command] #[specta::specta] async fn remove_account(user_id: String, state: State<'_, AppState>) -> Result<(), VRCError> { state .account_store .remove_account(&user_id) .await .map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn clear_all_accounts(state: State<'_, AppState>) -> Result<(), VRCError> { state .account_store .clear_all_accounts() .await .map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn load_last_account(state: State<'_, AppState>) -> Result, VRCError> { let account = match state .account_store .get_last_active_account() .await .map_err(|e| VRCError::unknown(e))? { Some(acc) => acc, None => return Ok(None), }; let client = state.vrchat_client.lock().await; client .import_cookies(account.auth_cookie, account.two_factor_cookie) .await; match client.get_current_user().await { Ok(user) => Ok(Some(user)), Err(_) => { // TODO: Handle if last account cannot be loaded // client.clear_cookies().await; Ok(None) } } } // Settings Commands #[tauri::command] #[specta::specta] async fn get_settings(state: State<'_, AppState>) -> Result { state .settings_store .get_settings() .await .map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn save_settings(settings: AppSettings, state: State<'_, AppState>) -> Result<(), VRCError> { info!( "Saving settings: developer_mode={}", settings.developer_mode ); state .settings_store .save_settings(settings) .await .map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn get_developer_mode(state: State<'_, AppState>) -> Result { state .settings_store .get_developer_mode() .await .map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn set_developer_mode(enabled: bool, state: State<'_, AppState>) -> Result<(), VRCError> { info!("Setting developer mode: {}", enabled); state .settings_store .set_developer_mode(enabled) .await .map_err(|e| VRCError::unknown(e)) } // Log Commands #[tauri::command] #[specta::specta] async fn get_backend_logs() -> Result, VRCError> { LogManager::read_logs().map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn clear_backend_logs() -> Result<(), VRCError> { info!("Clearing backend logs"); LogManager::clear_logs().map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn export_backend_logs() -> Result { LogManager::export_logs().map_err(|e| VRCError::unknown(e)) } // VRChat Service Status Commands #[tauri::command] #[specta::specta] async fn get_vrchat_status() -> Result { vrchat_status::fetch_vrchat_status().await } // Database Studio Commands #[tauri::command] #[specta::specta] async fn db_list_tables() -> Result, VRCError> { let studio = DatabaseStudio::new() .await .map_err(|e| VRCError::unknown(e))?; studio.list_tables().await.map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn db_get_table_schema(table_name: String) -> Result, VRCError> { let studio = DatabaseStudio::new() .await .map_err(|e| VRCError::unknown(e))?; studio .get_table_schema(&table_name) .await .map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn db_get_table_data( table_name: String, limit: Option, offset: Option, ) -> Result { let studio = DatabaseStudio::new() .await .map_err(|e| VRCError::unknown(e))?; studio .get_table_data(&table_name, limit, offset) .await .map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn db_get_table_count(table_name: String) -> Result { let studio = DatabaseStudio::new() .await .map_err(|e| VRCError::unknown(e))?; studio .get_table_count(&table_name) .await .map_err(|e| VRCError::unknown(e)) } #[tauri::command] #[specta::specta] async fn db_execute_query(query: String) -> Result { let studio = DatabaseStudio::new() .await .map_err(|e| VRCError::unknown(e))?; studio .execute_query(&query) .await .map_err(|e| VRCError::unknown(e)) } // Binding Generation fn create_specta_builder() -> SpectaBuilder { SpectaBuilder::::new() .commands(collect_commands![ vrchat_login, vrchat_verify_2fa, vrchat_get_current_user, vrchat_update_status, vrchat_logout, vrchat_get_online_friends, vrchat_get_uploaded_worlds, vrchat_get_uploaded_avatars, get_online_friends, get_all_friends, get_user, get_user_by_id, is_friend, is_user_online, vrchat_check_session, vrchat_clear_session, websocket_start, websocket_stop, save_current_account, get_all_accounts, switch_account, remove_account, clear_all_accounts, load_last_account, get_settings, save_settings, get_developer_mode, set_developer_mode, get_backend_logs, clear_backend_logs, export_backend_logs, get_vrchat_status, db_list_tables, db_get_table_schema, db_get_table_data, db_get_table_count, db_execute_query, check_image_cached, cache_image, get_cache_directory, ]) // Core VRChat API types .typ::() .typ::() .typ::() .typ::() // Enum types .typ::() .typ::() .typ::() .typ::() .typ::() .typ::() .typ::() .typ::() // User-related types .typ::() .typ::() .typ::() .typ::() .typ::() .typ::() // World types .typ::() .typ::() // Avatar types .typ::() .typ::() .typ::() // Store types .typ::() .typ::() // Log types .typ::() // Database types .typ::() .typ::() .typ::() // Status types .typ::() .typ::() .typ::() } pub fn generate_bindings() { use std::{fs, fs::File, io::Write}; eprintln!("Generating TypeScript bindings..."); let specta_builder = create_specta_builder(); let formatter = Typescript::default(); let bindings = specta_builder .export_str(&formatter) .expect("Failed to generate TypeScript bindings"); let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); let output_path = manifest_dir .join("..") .join("src") .join("types") .join("bindings.ts"); if let Some(parent) = output_path.parent() { fs::create_dir_all(parent).expect("Failed to create bindings directory"); } let mut file = File::create(&output_path).expect("Failed to create TypeScript bindings file"); file.write_all( b"// @ts-nocheck\n// This file is auto-generated by Specta. Do not edit manually.\n\n", ) .expect("Failed to write bindings header"); file.write_all(bindings.as_bytes()) .expect("Failed to write TypeScript bindings"); formatter.format(&output_path).ok(); eprintln!( "Successfully generated bindings at {}", output_path.display() ); } // Application Entry Point #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let specta_builder = create_specta_builder(); #[cfg(debug_assertions)] { eprintln!("Auto-generating TypeScript bindings in development mode..."); generate_bindings(); } let vrchat_client = VRChatClient::new().expect("Failed to create VRChat client"); let account_store = tauri::async_runtime::block_on(AccountStore::new()).expect("Failed to create AccountStore"); let settings_store = tauri::async_runtime::block_on(SettingsStore::new()) .expect("Failed to create SettingsStore"); let image_cache = Arc::new( tauri::async_runtime::block_on(ImageCacheStore::new()) .expect("Failed to create ImageCacheStore"), ); let user_store = UserStore::new(); tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin( tauri_plugin_log::Builder::new() .target(tauri_plugin_log::Target::new( tauri_plugin_log::TargetKind::Stdout, )) .target(tauri_plugin_log::Target::new( tauri_plugin_log::TargetKind::LogDir { file_name: Some("vrc-circle".to_string()), }, )) .level(log::LevelFilter::Info) .build(), ) .invoke_handler(specta_builder.invoke_handler()) .setup(move |app| { // Initialize WebSocket with app handle and UserStore let websocket = VRChatWebSocket::new(app.handle().clone(), user_store.clone()); let app_state = AppState { vrchat_client: Arc::new(Mutex::new(vrchat_client)), account_store, websocket: Arc::new(Mutex::new(websocket)), user_store, settings_store, image_cache: image_cache.clone(), }; app.manage(app_state); specta_builder.mount_events(app); Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }