From 89286ba4591c3a5fbdccecb30be4650144814fa3 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 17 Oct 2025 02:26:01 -0400 Subject: [PATCH] tauri window wip --- .../commands/enqueue/task_enqueue_success.rs | 1 + .../core/events/generation_events/common.rs | 1 + .../lifecycle/startup/handle_tauri_startup.rs | 2 + .../state/data_dir/app_credentials_dir.rs | 8 + .../enum_conversion/generation_provider.rs | 1 + crates/desktop/artcraft/src/lib.rs | 8 + .../grok/commands/grok_open_login_command.rs | 38 ++++ .../src/services/grok/commands/mod.rs | 1 + .../desktop/artcraft/src/services/grok/mod.rs | 3 + .../grok/state/grok_credential_manager.rs | 173 +++++++++++++++++ .../grok/state/grok_serializable_state.rs | 60 ++++++ .../artcraft/src/services/grok/state/mod.rs | 2 + .../grok_login_webview_extract_cookies.rs | 44 +++++ .../grok_login_window_open.rs | 74 +++++++ .../grok_login_window_thread.rs | 182 ++++++++++++++++++ .../grok/windows/grok_login_window/mod.rs | 3 + .../artcraft/src/services/grok/windows/mod.rs | 1 + crates/desktop/artcraft/src/services/mod.rs | 1 + .../enums/src/common/generation_provider.rs | 10 +- 19 files changed, 612 insertions(+), 1 deletion(-) create mode 100644 crates/desktop/artcraft/src/services/grok/commands/grok_open_login_command.rs create mode 100644 crates/desktop/artcraft/src/services/grok/commands/mod.rs create mode 100644 crates/desktop/artcraft/src/services/grok/mod.rs create mode 100644 crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs create mode 100644 crates/desktop/artcraft/src/services/grok/state/grok_serializable_state.rs create mode 100644 crates/desktop/artcraft/src/services/grok/state/mod.rs create mode 100644 crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_webview_extract_cookies.rs create mode 100644 crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_window_open.rs create mode 100644 crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_window_thread.rs create mode 100644 crates/desktop/artcraft/src/services/grok/windows/grok_login_window/mod.rs create mode 100644 crates/desktop/artcraft/src/services/grok/windows/mod.rs diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/task_enqueue_success.rs b/crates/desktop/artcraft/src/core/commands/enqueue/task_enqueue_success.rs index c49e71a2cd..4ea87ff7c7 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/task_enqueue_success.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/task_enqueue_success.rs @@ -32,6 +32,7 @@ impl TaskEnqueueSuccess{ match self.provider { GenerationProvider::Artcraft => GenerationServiceProvider::Artcraft, GenerationProvider::Fal => GenerationServiceProvider::Fal, + GenerationProvider::Grok => GenerationServiceProvider::Grok, GenerationProvider::Midjourney => GenerationServiceProvider::Midjourney, GenerationProvider::Sora => GenerationServiceProvider::Sora, } diff --git a/crates/desktop/artcraft/src/core/events/generation_events/common.rs b/crates/desktop/artcraft/src/core/events/generation_events/common.rs index 3d45ce600e..c704aa87f1 100644 --- a/crates/desktop/artcraft/src/core/events/generation_events/common.rs +++ b/crates/desktop/artcraft/src/core/events/generation_events/common.rs @@ -5,6 +5,7 @@ use serde::Serialize; pub enum GenerationServiceProvider { Artcraft, Fal, + Grok, Midjourney, Sora, } diff --git a/crates/desktop/artcraft/src/core/lifecycle/startup/handle_tauri_startup.rs b/crates/desktop/artcraft/src/core/lifecycle/startup/handle_tauri_startup.rs index 98904ef5b0..5464035eb4 100644 --- a/crates/desktop/artcraft/src/core/lifecycle/startup/handle_tauri_startup.rs +++ b/crates/desktop/artcraft/src/core/lifecycle/startup/handle_tauri_startup.rs @@ -9,6 +9,7 @@ use crate::core::lifecycle::startup::tasks::spawn_storyteller_threads::spawn_sto use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; use crate::core::state::artcraft_platform_info::ArtcraftPlatformInfo; use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::services::grok::state::grok_credential_manager::GrokCredentialManager; use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager; use crate::services::midjourney::threads::midjourney_long_polling_thread::midjourney_long_polling_thread; use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; @@ -26,6 +27,7 @@ pub async fn handle_tauri_startup( sora_credential_manager: SoraCredentialManager, sora_task_queue: SoraTaskQueue, mj_creds_manager: MidjourneyCredentialManager, + grok_creds_manager: GrokCredentialManager, ) -> AnyhowResult<()> { set_app_log_level( diff --git a/crates/desktop/artcraft/src/core/state/data_dir/app_credentials_dir.rs b/crates/desktop/artcraft/src/core/state/data_dir/app_credentials_dir.rs index b25ff4a37c..3e52aea794 100644 --- a/crates/desktop/artcraft/src/core/state/data_dir/app_credentials_dir.rs +++ b/crates/desktop/artcraft/src/core/state/data_dir/app_credentials_dir.rs @@ -21,6 +21,14 @@ impl DataSubdir for AppCredentialsDir { } impl AppCredentialsDir { + pub fn get_grok_state_path(&self) -> PathBuf { + self.path.join("grok_state.json") + } + + pub fn get_grok_cookies_path(&self) -> PathBuf { + self.path.join("grok_cookies.txt") + } + pub fn get_sora_cookie_file_path(&self) -> PathBuf { self.path.join("sora_cookies.txt") } diff --git a/crates/desktop/artcraft/src/core/utils/enum_conversion/generation_provider.rs b/crates/desktop/artcraft/src/core/utils/enum_conversion/generation_provider.rs index a9b906fe6e..1ec6cc92c3 100644 --- a/crates/desktop/artcraft/src/core/utils/enum_conversion/generation_provider.rs +++ b/crates/desktop/artcraft/src/core/utils/enum_conversion/generation_provider.rs @@ -6,6 +6,7 @@ pub fn to_generation_service_provider(provider: GenerationProvider) -> Generatio match provider { GenerationProvider::Artcraft => GenerationServiceProvider::Artcraft, GenerationProvider::Fal => GenerationServiceProvider::Fal, + GenerationProvider::Grok => GenerationServiceProvider::Grok, GenerationProvider::Midjourney => GenerationServiceProvider::Midjourney, GenerationProvider::Sora => GenerationServiceProvider::Sora, } diff --git a/crates/desktop/artcraft/src/lib.rs b/crates/desktop/artcraft/src/lib.rs index f5dcf75c94..eaa371bd11 100644 --- a/crates/desktop/artcraft/src/lib.rs +++ b/crates/desktop/artcraft/src/lib.rs @@ -29,6 +29,8 @@ use crate::core::state::data_dir::app_data_root::AppDataRoot; use crate::core::state::provider_priority::ProviderPriorityStore; use crate::core::threads::discord_presence_thread::discord_presence_thread; use crate::core::threads::main_window_thread::main_window_thread::main_window_thread; +use crate::services::grok::commands::grok_open_login_command::grok_open_login_command; +use crate::services::grok::state::grok_credential_manager::GrokCredentialManager; use crate::services::midjourney::commands::midjourney_clear_credentials_command::midjourney_clear_credentials_command; use crate::services::midjourney::commands::midjourney_get_credential_info_command::midjourney_get_credential_info_command; use crate::services::midjourney::commands::midjourney_open_login_command::midjourney_open_login_command; @@ -97,6 +99,9 @@ pub fn run() { let midjourney_creds_manager = MidjourneyCredentialManager::initialize_from_disk_infallible(&app_data_root); let midjourney_creds_manager_2 = midjourney_creds_manager.clone(); + let grok_creds_manager = GrokCredentialManager::initialize_from_disk_infallible(&app_data_root); + let grok_creds_manager_2 = grok_creds_manager.clone(); + println!("Initializing backend runtime..."); let builder = tauri::Builder::default() @@ -134,6 +139,7 @@ pub fn run() { sora_creds, sora_tasks, midjourney_creds_manager_2, + grok_creds_manager_2, ).await; if let Err(err) = result { @@ -148,6 +154,7 @@ pub fn run() { .manage(app_env_configs) .manage(app_preferences) .manage(artcraft_platform_info) + .manage(grok_creds_manager) .manage(midjourney_creds_manager) .manage(sora_creds_manager) .manage(sora_task_queue) @@ -168,6 +175,7 @@ pub fn run() { get_app_preferences_command, get_provider_order_command, get_task_queue_command, + grok_open_login_command, load_without_cors_command, mark_task_as_dismissed_command, media_file_delete_command, diff --git a/crates/desktop/artcraft/src/services/grok/commands/grok_open_login_command.rs b/crates/desktop/artcraft/src/services/grok/commands/grok_open_login_command.rs new file mode 100644 index 0000000000..6d60b4155f --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/commands/grok_open_login_command.rs @@ -0,0 +1,38 @@ +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::services::grok::windows::grok_login_window::grok_login_window_open::grok_login_window_open; +use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager; +use errors::AnyhowResult; +use log::{error, info}; +use tauri::{AppHandle, State}; +use crate::services::grok::state::grok_credential_manager::GrokCredentialManager; + +#[tauri::command] +pub async fn grok_open_login_command( + app: AppHandle, + app_data_root: State<'_, AppDataRoot>, + grok_creds_manager: State<'_, GrokCredentialManager>, +) -> Result { + info!("grok_open_login_command called"); + + do_open_login(&app, &app_data_root, &grok_creds_manager) + .await + .map_err(|err| { + error!("Error opening login: {:?}", err); + format!("Error opening login: {:?}", err) + })?; + + Ok("result".to_string()) +} + +async fn do_open_login( + app: &AppHandle, + app_data_root: &AppDataRoot, + grok_creds_manager: &GrokCredentialManager, +) -> AnyhowResult<()> { + info!("Building login window..."); + + grok_login_window_open(app, app_data_root, grok_creds_manager).await?; + + info!("Done."); + Ok(()) +} diff --git a/crates/desktop/artcraft/src/services/grok/commands/mod.rs b/crates/desktop/artcraft/src/services/grok/commands/mod.rs new file mode 100644 index 0000000000..9241b5274e --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/commands/mod.rs @@ -0,0 +1 @@ +pub mod grok_open_login_command; diff --git a/crates/desktop/artcraft/src/services/grok/mod.rs b/crates/desktop/artcraft/src/services/grok/mod.rs new file mode 100644 index 0000000000..132c03a929 --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/mod.rs @@ -0,0 +1,3 @@ +pub mod commands; +pub mod state; +pub mod windows; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs b/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs new file mode 100644 index 0000000000..e6def8dbca --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs @@ -0,0 +1,173 @@ +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::services::grok::state::grok_serializable_state::{GrokSerializableState, SERIALIZABLE_GROK_STATE_VERSION}; +use cookie_store::cookie_store::CookieStore; +use errors::AnyhowResult; +use log::{info, warn}; +use std::fs::read_to_string; +use std::sync::{Arc, RwLock}; + +#[derive(Clone)] +pub struct GrokCredentialManager { + // TODO: Put last write/last read timestamps on these. + cookies: Arc>>, + //user_info: Arc>>, + app_data_root: AppDataRoot, +} + +impl GrokCredentialManager { + pub fn initialize_empty(app_data_root: &AppDataRoot) -> Self { + Self { + cookies: Arc::new(RwLock::new(None)), + app_data_root: app_data_root.clone(), + } + } + + pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self { + let mut cookies; + //let mut user_info; + + let result = GrokSerializableState::read_from_disk(app_data_root); + match result { + Err(err) => { + warn!("Failed to read midjourney state from disk: {:?}", err); + cookies = Arc::new(RwLock::new(None)); + //user_info = Arc::new(RwLock::new(None)); + }, + Ok(None) => { + cookies = Arc::new(RwLock::new(None)); + //user_info = Arc::new(RwLock::new(None)); + } + Ok(Some(state)) => { + let maybe_cookies = state.user_cookies + .map(|cookies| cookies.to_cookie_store()); + //let maybe_user_info = state.user_info + // .map(|info| info.to_user_info()); + cookies = Arc::new(RwLock::new(maybe_cookies)); + //user_info = Arc::new(RwLock::new(maybe_user_info)); + } + }; + + Self { + cookies, + app_data_root: app_data_root.clone(), + } + } + + pub fn maybe_copy_cookie_store(&self) -> anyhow::Result> { + match self.cookies.read() { + Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), + Ok(store) => Ok(store.clone()), + } + } + + //pub fn maybe_copy_user_info(&self) -> anyhow::Result> { + // match self.user_info.read() { + // Err(err) => Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), + // Ok(info) => Ok(info.clone()), + // } + //} + + pub fn replace_cookie_store(&self, store: CookieStore) -> anyhow::Result<()> { + match self.cookies.write() { + Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), + Ok(mut current_store) => { + *current_store = Some(store); + Ok(()) + } + } + } + + //pub fn replace_user_info(&self, user_info: MidjourneyUserInfo) -> anyhow::Result<()> { + // match self.user_info.write() { + // Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), + // Ok(mut current_info) => { + // *current_info = Some(user_info); + // Ok(()) + // } + // } + //} + + // NB: This is just a heuristic. We'll add better checks later. + pub fn session_appears_active(&self) -> anyhow::Result { + let maybe_cookies = match self.cookies.read() { + Err(err) => return Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), + Ok(store) => store.clone(), + }; + + //let maybe_user_info = match self.user_info.read() { + // Err(err) => return Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), + // Ok(info) => info.clone(), + //}; + + if maybe_cookies.is_none() { + return Ok(false); + } + + let cookies = maybe_cookies.unwrap(); + //let user_info = maybe_user_info.unwrap(); + + // NB: We consider the session active if we have auth cookies and a user id. + //let has_user_id = user_info.user_id.is_some(); + //let maybe_has_auth_cookies = cookie_store_has_auth_cookies(&cookies); + let maybe_has_auth_cookies = true; // TODO TODO TODO - fix this + + // Misc cookies without login cookies are ~1055 length + // AUTH_I is ~1500 length + // AUTH_R is ~500 length + let maybe_has_big_enough_cookie = cookies.calculate_approx_cookie_character_length() > 2100; + + // TODO: Heuristic should count. + // TODO: Consolidate with "login window thread" logic. + // TODO: Check timestamp of last valid request. + Ok(maybe_has_auth_cookies && maybe_has_big_enough_cookie && maybe_has_big_enough_cookie) + } + + pub fn clear_credentials(&self) -> anyhow::Result<()> { + match self.cookies.write() { + Err(err) => return Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), + Ok(mut store) => *store = None, + } + //match self.user_info.write() { + // Err(err) => return Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), + // Ok(mut info) => *info = None, + //} + Ok(()) + } + + pub fn persist_to_disk(&self) -> anyhow::Result<()> { + let maybe_cookies = match self.cookies.read() { + Err(err) => return Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), + Ok(store) => store.clone(), + }; + + //let maybe_user_info = match self.user_info.read() { + // Err(err) => return Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), + // Ok(info) => info.clone(), + //}; + + let maybe_cookies= maybe_cookies + .map(|cookies| cookies.to_serializable()); + + //let maybe_user_info= maybe_user_info + // .map(|info| info.to_serializable()); + + if maybe_cookies.is_none() { + info!("Nothing to write to disk, skipping."); + return Ok(()); + + } + + let state = GrokSerializableState { + version: SERIALIZABLE_GROK_STATE_VERSION, + user_cookies: maybe_cookies, + }; + + let path = self.app_data_root.credentials_dir().get_midjourney_state_path(); + let serialized = serde_json::to_string(&state)?; + + std::fs::write(path, serialized)?; + + Ok(()) + } +} + diff --git a/crates/desktop/artcraft/src/services/grok/state/grok_serializable_state.rs b/crates/desktop/artcraft/src/services/grok/state/grok_serializable_state.rs new file mode 100644 index 0000000000..0121fb9c4e --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/state/grok_serializable_state.rs @@ -0,0 +1,60 @@ +use std::fs::read_to_string; +use cookie_store::serialized_cookie_store::SerializableCookieStore; +use serde_derive::{Deserialize, Serialize}; +use crate::core::state::data_dir::app_data_root::AppDataRoot; + +pub (super) const SERIALIZABLE_GROK_STATE_VERSION: u32 = 1; + +#[derive(Serialize, Deserialize)] +pub (super) struct GrokSerializableState { + pub (super) version: u32, + + #[serde(skip_serializing_if = "Option::is_none")] + pub (super) user_cookies: Option, + + //#[serde(skip_serializing_if = "Option::is_none")] + //pub (super) user_info: Option, +} + +//#[derive(Serialize, Deserialize)] +//pub (super) struct SerializableMidjourneyUserInfo { +// #[serde(skip_serializing_if = "Option::is_none")] +// pub (super) user_id: Option, +// +// #[serde(skip_serializing_if = "Option::is_none")] +// pub (super) email: Option, +// +// #[serde(skip_serializing_if = "Option::is_none")] +// pub (super) websocket_token: Option, +//} + +//impl SerializableMidjourneyUserInfo { +// pub fn to_user_info(&self) -> MidjourneyUserInfo { +// MidjourneyUserInfo { +// user_id: self.user_id +// .as_deref() +// .map(|id| MidjourneyUserId::from_str(id)), +// email: self.email.clone(), +// websocket_token: self.websocket_token.clone(), +// } +// } +//} + + +impl GrokSerializableState { + pub fn read_from_disk(root: &AppDataRoot) -> anyhow::Result> { + let midjourney_state_path= root.credentials_dir().get_grok_state_path(); + + if !midjourney_state_path.exists() { + return Ok(None); + } + + let contents = read_to_string(&midjourney_state_path)? + .trim() + .to_string(); + + let state: GrokSerializableState = serde_json::from_str(&contents)?; + + Ok(Some(state)) + } +} \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/grok/state/mod.rs b/crates/desktop/artcraft/src/services/grok/state/mod.rs new file mode 100644 index 0000000000..dc62d868a1 --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/state/mod.rs @@ -0,0 +1,2 @@ +pub mod grok_credential_manager; +pub mod grok_serializable_state; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_webview_extract_cookies.rs b/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_webview_extract_cookies.rs new file mode 100644 index 0000000000..74bf3757ef --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_webview_extract_cookies.rs @@ -0,0 +1,44 @@ +use cookie_store::cookie_store::CookieStore; +use errors::AnyhowResult; +use once_cell::sync::Lazy; +use reqwest::Url; +use std::collections::HashSet; +use tauri::webview::Cookie; +use tauri::WebviewWindow; + +static WWW_COOKIE_URL: Lazy = Lazy::new(|| { + Url::parse("https://www.grok.com").expect("URL should parse") +}); + +static ROOT_COOKIE_URL: Lazy = Lazy::new(|| { + Url::parse("https://grok.com").expect("URL should parse") +}); + +pub fn grok_login_webview_extract_cookies(webview: &WebviewWindow) -> AnyhowResult { + let mut cookie_store = CookieStore::empty(); + let cookies = get_all_grok_cookies(webview)?; + for cookie in cookies.iter() { + cookie_store.add_cookie_name_and_value( + cookie.name().to_string(), + cookie.value().to_string(), + ); + } + Ok(cookie_store) +} + +fn get_all_grok_cookies(webview: &WebviewWindow) -> AnyhowResult> { + let www_cookies = webview.cookies_for_url(WWW_COOKIE_URL.clone())?; + let root_cookies = webview.cookies_for_url(ROOT_COOKIE_URL.clone())?; + + let mut all_cookies = www_cookies; + let mut cookie_names = HashSet::new(); + + for cookie in root_cookies.iter() { + if !cookie_names.contains(cookie.name()) { + cookie_names.insert(cookie.name().to_string()); + all_cookies.push(cookie.clone()); + } + } + + Ok(all_cookies) +} diff --git a/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_window_open.rs b/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_window_open.rs new file mode 100644 index 0000000000..2eeb173593 --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_window_open.rs @@ -0,0 +1,74 @@ +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::utils::clear_all_webview_cookies::clear_all_webview_cookies; +use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager; +use crate::services::midjourney::windows::midjourney_login_window_thread::midjourney_login_window_thread; +use anyhow::anyhow; +use errors::AnyhowResult; +use once_cell::sync::Lazy; +use reqwest::Url; +use std::time::Duration; +use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; +use crate::services::grok::state::grok_credential_manager::GrokCredentialManager; +use crate::services::grok::windows::grok_login_window::grok_login_window_thread::grok_login_window_thread; + +/// Name of the window +pub (super) const GROK_LOGIN_WINDOW_NAME: &str = "grok_login_window"; + +pub (super) static START_URL: Lazy = Lazy::new(|| { + Url::parse("https://google.com").expect("URL should parse") +}); + +pub (super) static GROK_HOMEPAGE_URL: Lazy = Lazy::new(|| { + Url::parse("https://grok.com/").expect("URL should parse") +}); + +pub (super) static GROK_LOGIN_URL: Lazy = Lazy::new(|| { + Url::parse("https://accounts.x.ai/sign-in?redirect=grok-com").expect("URL should parse") +}); + +pub async fn grok_login_window_open( + app: &AppHandle, + app_data_root: &AppDataRoot, + grok_creds_manager: &GrokCredentialManager, +) -> AnyhowResult<()> { + if app.get_window(GROK_LOGIN_WINDOW_NAME).is_some() { + return Err(anyhow!("Login window already open")); + } + + let url = WebviewUrl::External(START_URL.clone()); + + let window = WebviewWindowBuilder::new(app, GROK_LOGIN_WINDOW_NAME, url) + //.user_agent(openai_sora_client::credentials::USER_AGENT) + .always_on_top(false) + .title("Login to Grok") + .center() + .resizable(true) + .visible(true) + .closable(true) + .min_inner_size(200.0, 800.0) + .focused(true) + .devtools(true) + .build()?; + + let webview = window.get_webview(GROK_LOGIN_WINDOW_NAME) + .ok_or_else(|| anyhow!("no webview found"))?; + + clear_all_webview_cookies(&webview)?; + + webview.navigate(GROK_HOMEPAGE_URL.clone())?; + + // NB: We're starting to get Cloudflare protection screens. Let's try to avoid. + tokio::time::sleep(Duration::from_millis(100)).await; + + webview.navigate(GROK_LOGIN_URL.clone())?; + + let app_handle = app.clone(); + let app_data_root = app_data_root.clone(); + let grok_creds_manager= grok_creds_manager.clone(); + + let _ = tauri::async_runtime::spawn(async move { + grok_login_window_thread(app_handle, app_data_root, grok_creds_manager).await; + }); + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_window_thread.rs b/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_window_thread.rs new file mode 100644 index 0000000000..672e1d011c --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/grok_login_window_thread.rs @@ -0,0 +1,182 @@ +use crate::core::events::basic_sendable_event_trait::BasicSendableEvent; +use crate::core::events::functional_events::refresh_account_state_event::RefreshAccountStateEvent; +use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider}; +use crate::core::events::generation_events::generation_complete_event::GenerationCompleteEvent; +use crate::core::events::sendable_event_trait::SendableEvent; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::utils::window::get_webview_window_hostname::get_webview_window_hostname; +use crate::services::grok::state::grok_credential_manager::GrokCredentialManager; +use crate::services::grok::windows::grok_login_window::grok_login_webview_extract_cookies::grok_login_webview_extract_cookies; +use crate::services::grok::windows::grok_login_window::grok_login_window_open::GROK_LOGIN_WINDOW_NAME; +use crate::services::sora::events::sora_login_success_event::SoraLoginSuccessEvent; +use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; +use crate::services::sora::windows::sora_login_window::extract_sora_webview_cookies::extract_sora_webview_cookies; +use anyhow::anyhow; +use cookie_store::cookie_store::CookieStore; +use enums::common::generation_provider::GenerationProvider; +use errors::AnyhowResult; +use log::{error, info}; +use openai_sora_client::creds::sora_credential_set::SoraCredentialSet; +use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; +use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence}; +use tauri::{AppHandle, Manager, WebviewWindow}; + +pub async fn grok_login_window_thread( + app: AppHandle, + app_data_root: AppDataRoot, + grok_creds_manager: GrokCredentialManager, +) { + let mut visited_login = false; + + loop { + let login_webview_window = match app.get_webview_window(GROK_LOGIN_WINDOW_NAME) { + Some(webview) => webview, + None => { + info!("Exit Grok login thread."); + return; // NB: Only exit if we don't have the webview. + } + }; + + let result = check_login_window( + &app, + &login_webview_window, + &app_data_root, + &grok_creds_manager, + &mut visited_login, + ).await; + + match result { + Err(err) => { + error!("Error checking Grok login window: {:?}", err); + } + Ok(false) => {} // Continue iteration and try again... + Ok(true) => { + info!("Successfully saved cookies from login window. Closing."); + if let Err(err) = login_webview_window.close() { + error!("Error closing login window: {:?}", err); + } + return; + } + } + + tokio::time::sleep(std::time::Duration::from_millis(2_000)).await; + } +} + +/// Returns true if we can exit. +async fn check_login_window( + app_handle: &AppHandle, + webview_window: &WebviewWindow, + app_data_root: &AppDataRoot, + grok_creds_manager: &GrokCredentialManager, + visited_login: &mut bool, +) -> AnyhowResult { + + /* Login flow looks like this: + + 1. Start: https://accounts.x.ai/sign-in?redirect=grok-com + 2. SSO / Google Login (etc): https://accounts.google.com/v3/signin/challenge/pwd?[query] + 3. Done / Landing: https://grok.com/... + */ + + let hostname = get_webview_window_hostname(webview_window)?; + + let mut maybe_at_destination = false; + + match hostname.as_str() { + "www.grok.com" | + "grok.com" + => { + maybe_at_destination = true; + } + // chatgpt.com/auth is also an auth domain + "accounts.x.ai" | + "auth.openai.com" | + "accounts.google.com" | + "accounts.youtube.com" | + "login.live.com" | + "appleid.apple.com" + => { + // NB: We're in auth flow. + info!("Grok webview is in auth flow; hostname `{}`.", hostname); + *visited_login = true; + return Ok(false) + } + _ => {}, // We just don't know... + } + + let cookie_store = grok_login_webview_extract_cookies(webview_window)?; + + let maybe_has_auth_cookies = true; //cookie_store_has_auth_cookies(&cookie_store); // TODO TODO FIXME + let maybe_has_enough_cookies = cookie_store.len() > 6; + let maybe_completed_login_cycle = *visited_login && maybe_at_destination; + + // Misc cookies without login cookies are ~1055 length + // AUTH_I is ~1500 length + // AUTH_R is ~500 length + let maybe_has_big_enough_cookie = cookie_store.calculate_approx_cookie_character_length() > 2100; + + let mut heuristic_count = 0; + + if maybe_has_auth_cookies { + heuristic_count += 1; + } + if maybe_has_enough_cookies { + heuristic_count += 1; + } + if maybe_completed_login_cycle { + heuristic_count += 1; + } + if maybe_has_big_enough_cookie { + heuristic_count += 1; + } + + if heuristic_count < 3 { + return Ok(false); + } + + info!("Heuristic count is ({}); we're likely done.", heuristic_count); + + info!("Current cookies (len {}): {:?}", cookie_store.len(), cookie_store.to_cookie_string()); + + //let response = get_user_info(GetUserInfoRequest { + // hostname: MidjourneyHostname::Standard, + // cookie_header: cookie_store.to_cookie_string(), + //}).await; + + //match response { + // Err(err) => { + // error!("Error getting midjourney user info: {:?}", err); + // // NB: Fall through. Allow us to update the cookies. + // } + // Ok(user_info) => { + // info!("Got midjourney user info: {:?}", user_info); + // let user_info = MidjourneyUserInfo::from_api_response(user_info); + // + // //if let Err(err) = grok_creds_manager.replace_user_info(user_info) { + // // error!("Error saving midjourney user info: {:?}", err); + // // // NB: Fall through. Allow us to update the cookies. + // //} + // } + //} + + grok_creds_manager.replace_cookie_store(cookie_store)?; + + let result = grok_creds_manager.persist_to_disk(); + + if let Err(err) = result { + error!("Error persisting grok cookies to disk: {:?}", err); + return Ok(false); + } + + let event = RefreshAccountStateEvent { + provider: Some(GenerationProvider::Grok), + }; + + if let Err(err) = event.send(&app_handle) { + error!("Failed to send RefreshAccountStateEvent: {:?}", err); // Fail open + } + + Ok(true) +} + diff --git a/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/mod.rs b/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/mod.rs new file mode 100644 index 0000000000..5f818a63fe --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/windows/grok_login_window/mod.rs @@ -0,0 +1,3 @@ +pub mod grok_login_window_open; +pub (super) mod grok_login_window_thread; +pub (super) mod grok_login_webview_extract_cookies; diff --git a/crates/desktop/artcraft/src/services/grok/windows/mod.rs b/crates/desktop/artcraft/src/services/grok/windows/mod.rs new file mode 100644 index 0000000000..66ef2bb3e8 --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/windows/mod.rs @@ -0,0 +1 @@ +pub mod grok_login_window; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/mod.rs b/crates/desktop/artcraft/src/services/mod.rs index 97183d0ba7..f7480cf88b 100644 --- a/crates/desktop/artcraft/src/services/mod.rs +++ b/crates/desktop/artcraft/src/services/mod.rs @@ -1,3 +1,4 @@ +pub mod grok; pub mod midjourney; pub mod sora; pub mod storyteller; \ No newline at end of file diff --git a/crates/schema/public/enums/src/common/generation_provider.rs b/crates/schema/public/enums/src/common/generation_provider.rs index 4be9e10c7e..e365fd603a 100644 --- a/crates/schema/public/enums/src/common/generation_provider.rs +++ b/crates/schema/public/enums/src/common/generation_provider.rs @@ -15,6 +15,7 @@ use utoipa::ToSchema; pub enum GenerationProvider { Artcraft, Fal, + Grok, Midjourney, Sora, } @@ -33,6 +34,7 @@ impl GenerationProvider { match self { Self::Artcraft => "artcraft", Self::Fal => "fal", + Self::Grok => "grok", Self::Midjourney => "midjourney", Self::Sora => "sora", } @@ -42,6 +44,7 @@ impl GenerationProvider { match value { "artcraft" => Ok(Self::Artcraft), "fal" => Ok(Self::Fal), + "grok" => Ok(Self::Grok), "midjourney" => Ok(Self::Midjourney), "sora" => Ok(Self::Sora), _ => Err(EnumError::CouldNotConvertFromString(value.to_string())), @@ -54,6 +57,7 @@ impl GenerationProvider { BTreeSet::from([ Self::Artcraft, Self::Fal, + Self::Grok, Self::Midjourney, Self::Sora, ]) @@ -73,6 +77,7 @@ mod tests { fn test_serialization() { assert_serialization(GenerationProvider::Artcraft, "artcraft"); assert_serialization(GenerationProvider::Fal, "fal"); + assert_serialization(GenerationProvider::Grok, "grok"); assert_serialization(GenerationProvider::Midjourney, "midjourney"); assert_serialization(GenerationProvider::Sora, "sora"); } @@ -81,6 +86,7 @@ mod tests { fn to_str() { assert_eq!(GenerationProvider::Artcraft.to_str(), "artcraft"); assert_eq!(GenerationProvider::Fal.to_str(), "fal"); + assert_eq!(GenerationProvider::Grok.to_str(), "grok"); assert_eq!(GenerationProvider::Midjourney.to_str(), "midjourney"); assert_eq!(GenerationProvider::Sora.to_str(), "sora"); } @@ -89,6 +95,7 @@ mod tests { fn from_str() { assert_eq!(GenerationProvider::from_str("artcraft").unwrap(), GenerationProvider::Artcraft); assert_eq!(GenerationProvider::from_str("fal").unwrap(), GenerationProvider::Fal); + assert_eq!(GenerationProvider::from_str("grok").unwrap(), GenerationProvider::Grok); assert_eq!(GenerationProvider::from_str("midjourney").unwrap(), GenerationProvider::Midjourney); assert_eq!(GenerationProvider::from_str("sora").unwrap(), GenerationProvider::Sora); } @@ -107,9 +114,10 @@ mod tests { #[test] fn all_variants() { let mut variants = GenerationProvider::all_variants(); - assert_eq!(variants.len(), 4); + assert_eq!(variants.len(), 5); assert_eq!(variants.pop_first(), Some(GenerationProvider::Artcraft)); assert_eq!(variants.pop_first(), Some(GenerationProvider::Fal)); + assert_eq!(variants.pop_first(), Some(GenerationProvider::Grok)); assert_eq!(variants.pop_first(), Some(GenerationProvider::Midjourney)); assert_eq!(variants.pop_first(), Some(GenerationProvider::Sora)); assert_eq!(variants.pop_first(), None);