mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 09:28:11 +00:00
tauri window wip
This commit is contained in:
1 parent
512059c0f5
commit
89286ba459
19 files changed
+612
-1
No files matched your search
@@ -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,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use serde::Serialize;
|
||||
pub enum GenerationServiceProvider {
|
||||
Artcraft,
|
||||
Fal,
|
||||
Grok,
|
||||
Midjourney,
|
||||
Sora,
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String, String> {
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod grok_open_login_command;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod commands;
|
||||
pub mod state;
|
||||
pub mod windows;
|
||||
@@ -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<RwLock<Option<CookieStore>>>,
|
||||
//user_info: Arc<RwLock<Option<MidjourneyUserInfo>>>,
|
||||
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<Option<CookieStore>> {
|
||||
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<Option<MidjourneyUserInfo>> {
|
||||
// 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<bool> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SerializableCookieStore>,
|
||||
|
||||
//#[serde(skip_serializing_if = "Option::is_none")]
|
||||
//pub (super) user_info: Option<SerializableMidjourneyUserInfo>,
|
||||
}
|
||||
|
||||
//#[derive(Serialize, Deserialize)]
|
||||
//pub (super) struct SerializableMidjourneyUserInfo {
|
||||
// #[serde(skip_serializing_if = "Option::is_none")]
|
||||
// pub (super) user_id: Option<String>,
|
||||
//
|
||||
// #[serde(skip_serializing_if = "Option::is_none")]
|
||||
// pub (super) email: Option<String>,
|
||||
//
|
||||
// #[serde(skip_serializing_if = "Option::is_none")]
|
||||
// pub (super) websocket_token: Option<String>,
|
||||
//}
|
||||
|
||||
//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<Option<GrokSerializableState>> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod grok_credential_manager;
|
||||
pub mod grok_serializable_state;
|
||||
+44
@@ -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<Url> = Lazy::new(|| {
|
||||
Url::parse("https://www.grok.com").expect("URL should parse")
|
||||
});
|
||||
|
||||
static ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse("https://grok.com").expect("URL should parse")
|
||||
});
|
||||
|
||||
pub fn grok_login_webview_extract_cookies(webview: &WebviewWindow) -> AnyhowResult<CookieStore> {
|
||||
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<Vec<Cookie>> {
|
||||
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)
|
||||
}
|
||||
+74
@@ -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<Url> = Lazy::new(|| {
|
||||
Url::parse("https://google.com").expect("URL should parse")
|
||||
});
|
||||
|
||||
pub (super) static GROK_HOMEPAGE_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse("https://grok.com/").expect("URL should parse")
|
||||
});
|
||||
|
||||
pub (super) static GROK_LOGIN_URL: Lazy<Url> = 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(())
|
||||
}
|
||||
+182
@@ -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<bool> {
|
||||
|
||||
/* 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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod grok_login_window;
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod grok;
|
||||
pub mod midjourney;
|
||||
pub mod sora;
|
||||
pub mod storyteller;
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user