From 674fc0d01e8a2cf077fa6caf2a6b86cf23c1f325 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 13 Aug 2025 17:57:08 -0400 Subject: [PATCH] login window + UI --- .../src/endpoints/submit_job.rs | 2 + crates/desktop/artcraft/src/lib.rs | 4 +- .../clear_midjourney_credentials_command.rs | 0 .../src/services/midjourney/commands/mod.rs | 2 + .../commands/open_midjourney_login_command.rs | 38 +++++ .../artcraft/src/services/midjourney/mod.rs | 4 + .../src/services/midjourney/state/mod.rs | 0 .../src/services/midjourney/threads/mod.rs | 0 .../windows/midjourney_login_window_thread.rs | 148 ++++++++++++++++++ .../src/services/midjourney/windows/mod.rs | 2 + .../windows/open_midjourney_login_window.rs | 78 +++++++++ crates/desktop/artcraft/src/services/mod.rs | 1 + .../open_sora_login_window.rs | 14 +- .../AccountSettings/AccountSettingsPane.tsx | 5 +- .../MidjourneyAccountBlock.tsx | 107 +++++++++++++ 15 files changed, 394 insertions(+), 11 deletions(-) create mode 100644 crates/desktop/artcraft/src/services/midjourney/commands/clear_midjourney_credentials_command.rs create mode 100644 crates/desktop/artcraft/src/services/midjourney/commands/mod.rs create mode 100644 crates/desktop/artcraft/src/services/midjourney/commands/open_midjourney_login_command.rs create mode 100644 crates/desktop/artcraft/src/services/midjourney/mod.rs create mode 100644 crates/desktop/artcraft/src/services/midjourney/state/mod.rs create mode 100644 crates/desktop/artcraft/src/services/midjourney/threads/mod.rs create mode 100644 crates/desktop/artcraft/src/services/midjourney/windows/midjourney_login_window_thread.rs create mode 100644 crates/desktop/artcraft/src/services/midjourney/windows/mod.rs create mode 100644 crates/desktop/artcraft/src/services/midjourney/windows/open_midjourney_login_window.rs create mode 100644 frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/MidjourneyAccountBlock.tsx diff --git a/crates/api_clients/midjourney_client/src/endpoints/submit_job.rs b/crates/api_clients/midjourney_client/src/endpoints/submit_job.rs index b234c1133c..967a756f12 100644 --- a/crates/api_clients/midjourney_client/src/endpoints/submit_job.rs +++ b/crates/api_clients/midjourney_client/src/endpoints/submit_job.rs @@ -169,6 +169,8 @@ mod tests { use errors::AnyhowResult; use filesys::read_to_trimmed_string::read_to_trimmed_string; + // Get channel id via: + // https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=AIzaSyAjizp68NsH3JGUS0EyLXsChW4fN0A92tM #[ignore] #[tokio::test] async fn test() -> AnyhowResult<()> { diff --git a/crates/desktop/artcraft/src/lib.rs b/crates/desktop/artcraft/src/lib.rs index 22a0323ba5..84c27c6888 100644 --- a/crates/desktop/artcraft/src/lib.rs +++ b/crates/desktop/artcraft/src/lib.rs @@ -7,8 +7,8 @@ use crate::core::commands::app_preferences::get_app_preferences_command::get_app use crate::core::commands::app_preferences::update_app_preference_command::update_app_preferences_command; use crate::core::commands::enqueue::image::enqueue_text_to_image_command::enqueue_text_to_image_command; use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::enqueue_image_bg_removal_command; -use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::enqueue_image_inpaint_command; use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::enqueue_contextual_edit_image_command; +use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::enqueue_image_inpaint_command; use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::enqueue_image_to_3d_object_command; use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enqueue_image_to_video_command; use crate::core::commands::flip_image::flip_image; @@ -32,6 +32,7 @@ use crate::services::fal::commands::set_fal_api_key_command::set_fal_api_key_com use crate::services::fal::state::fal_credential_manager::FalCredentialManager; use crate::services::fal::state::fal_task_queue::FalTaskQueue; use crate::services::fal::threads::fal_task_polling_thread::fal_task_polling_thread; +use crate::services::midjourney::commands::open_midjourney_login_command::open_midjourney_login_command; use crate::services::sora::commands::check_sora_session_command::check_sora_session_command; use crate::services::sora::commands::open_sora_login_command::open_sora_login_command; use crate::services::sora::commands::sora_image_generation_command::sora_image_generation_command; @@ -159,6 +160,7 @@ pub fn run() { get_provider_order_command, load_without_cors_command, open_sora_login_command, + open_midjourney_login_command, platform_info_command, set_fal_api_key_command, set_provider_order_command, diff --git a/crates/desktop/artcraft/src/services/midjourney/commands/clear_midjourney_credentials_command.rs b/crates/desktop/artcraft/src/services/midjourney/commands/clear_midjourney_credentials_command.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/desktop/artcraft/src/services/midjourney/commands/mod.rs b/crates/desktop/artcraft/src/services/midjourney/commands/mod.rs new file mode 100644 index 0000000000..c69985a1b3 --- /dev/null +++ b/crates/desktop/artcraft/src/services/midjourney/commands/mod.rs @@ -0,0 +1,2 @@ +pub mod open_midjourney_login_command; +pub mod clear_midjourney_credentials_command; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/midjourney/commands/open_midjourney_login_command.rs b/crates/desktop/artcraft/src/services/midjourney/commands/open_midjourney_login_command.rs new file mode 100644 index 0000000000..d5d4dd67b9 --- /dev/null +++ b/crates/desktop/artcraft/src/services/midjourney/commands/open_midjourney_login_command.rs @@ -0,0 +1,38 @@ +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::services::midjourney::windows::open_midjourney_login_window::open_midjourney_login_window; +use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; +use crate::services::sora::windows::sora_login_window::open_sora_login_window::open_sora_login_window; +use errors::AnyhowResult; +use log::{error, info}; +use tauri::{AppHandle, State}; + +#[tauri::command] +pub async fn open_midjourney_login_command( + app: AppHandle, + app_data_root: State<'_, AppDataRoot>, + sora_creds_manager: State<'_, SoraCredentialManager>, +) -> Result { + info!("open_midjourney_command called"); + + do_open_login(&app, &app_data_root, &sora_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, + sora_creds_manager: &SoraCredentialManager, +) -> AnyhowResult<()> { + info!("Building login window..."); + + open_midjourney_login_window(app, app_data_root, sora_creds_manager).await?; + + info!("Done."); + Ok(()) +} diff --git a/crates/desktop/artcraft/src/services/midjourney/mod.rs b/crates/desktop/artcraft/src/services/midjourney/mod.rs new file mode 100644 index 0000000000..ad664c3b7d --- /dev/null +++ b/crates/desktop/artcraft/src/services/midjourney/mod.rs @@ -0,0 +1,4 @@ +pub mod threads; +pub mod state; +pub mod commands; +pub mod windows; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/midjourney/state/mod.rs b/crates/desktop/artcraft/src/services/midjourney/state/mod.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/desktop/artcraft/src/services/midjourney/threads/mod.rs b/crates/desktop/artcraft/src/services/midjourney/threads/mod.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/desktop/artcraft/src/services/midjourney/windows/midjourney_login_window_thread.rs b/crates/desktop/artcraft/src/services/midjourney/windows/midjourney_login_window_thread.rs new file mode 100644 index 0000000000..dc0cfb5dd2 --- /dev/null +++ b/crates/desktop/artcraft/src/services/midjourney/windows/midjourney_login_window_thread.rs @@ -0,0 +1,148 @@ +use crate::core::events::sendable_event_trait::SendableEvent; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::services::midjourney::windows::open_midjourney_login_window::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 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 midjourney_login_thread( + app: AppHandle, + app_data_root: AppDataRoot, + sora_creds_manager: SoraCredentialManager +) { + loop { + let login_webview_window = match app.get_webview_window(LOGIN_WINDOW_NAME) { + Some(webview) => webview, + None => { + info!("Exit midjourney 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, + &sora_creds_manager, + ).await; + + match result { + Err(err) => { + error!("Error checking 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, + sora_credential_manager: &SoraCredentialManager, +) -> AnyhowResult { + + /* Login flow looks like this: + + 1. Start: https://sora.chatgpt.com/ + 2. Login Start: https://chatgpt.com/auth/login?next=%2Fsora%2F [this is where we start] + 3. Login Continue: https://auth.openai.com/log-in + 4. SSO / Google Login (etc): https://accounts.google.com/v3/signin/challenge/pwd?[query] + 5. Done / Landing: https://sora.chatgpt.com/explore + */ + + /* + - Do not use credential manager until the end (we don't load old cookies!) + - Check if we're on the correct domain, if not exit? (or inverse, that we're not in login flow) + */ + + let hostname = get_hostname(webview_window)?; + + let mut maybe_at_destination = false; + + match hostname.as_str() { + "sora.com" | // Old destination + "sora.chatgpt.com" // New destination + => { + maybe_at_destination = true; + } + // chatgpt.com/auth is also an auth domain + "auth.openai.com" | + "accounts.google.com" | + "accounts.youtube.com" | + "login.live.com" | + "appleid.apple.com" + => { + // NB: We're in auth flow. + info!("Sora login webview is in auth flow; hostname `{}`.", hostname); + return Ok(false) + } + _ => {}, // We just don't know... + } + + let webview_cookies = extract_sora_webview_cookies(webview_window)?.trim().to_string(); + + let session_cookie_presence = has_session_cookie(&webview_cookies) + .unwrap_or_else(|err| { + error!("Failed to check for session cookie: {:?}", err); + SessionCookiePresence::MaybePresent + }); + + info!("Sora login webview is at hostname `{}`; cookie status: {:?}.", hostname, session_cookie_presence); + + match session_cookie_presence { + SessionCookiePresence::Absent => { + info!("Session cookies are absent."); + return Ok(false); + } + _ => {}, + } + + let mut new_credentials = + SoraCredentialSet::initialize_with_just_cookies_str(&webview_cookies); + + let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?; + + // TODO(bt): Race conditions ahead. + + sora_credential_manager.clear_credentials()?; + sora_credential_manager.try_purge_credentials_from_disk(); + + sora_credential_manager.set_credentials(&new_credentials)?; + sora_credential_manager.persist_all_to_disk()?; + + // NB: Event sent to the frontend for the login flow. We shouldn't rely on just this + // alone as it could be brittle if the events aren't caught. + let event = SoraLoginSuccessEvent {}; + + if let Err(err) = event.send(app_handle) { + error!("Error sending Sora login success event: {:?}", err); + } + + Ok(true) +} + +fn get_hostname(webview: &WebviewWindow) -> AnyhowResult { + let url = webview.url()?; + let url_hostname= url.host() + .ok_or(anyhow!("no host in url"))? + .to_string(); + Ok(url_hostname) +} diff --git a/crates/desktop/artcraft/src/services/midjourney/windows/mod.rs b/crates/desktop/artcraft/src/services/midjourney/windows/mod.rs new file mode 100644 index 0000000000..7865b5753a --- /dev/null +++ b/crates/desktop/artcraft/src/services/midjourney/windows/mod.rs @@ -0,0 +1,2 @@ +pub mod open_midjourney_login_window; +pub mod midjourney_login_window_thread; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/midjourney/windows/open_midjourney_login_window.rs b/crates/desktop/artcraft/src/services/midjourney/windows/open_midjourney_login_window.rs new file mode 100644 index 0000000000..1b3e7ec395 --- /dev/null +++ b/crates/desktop/artcraft/src/services/midjourney/windows/open_midjourney_login_window.rs @@ -0,0 +1,78 @@ +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::sora::state::sora_credential_manager::SoraCredentialManager; +use crate::services::sora::windows::sora_login_window::sora_login_thread::sora_login_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}; + +/// Name of the window +pub (super) const LOGIN_WINDOW_NAME: &str = "midjourney_login_window"; + +pub (super) const START_URL_STR: &str = "https://google.com"; + +pub (super) static START_URL: Lazy = Lazy::new(|| { + Url::parse(START_URL_STR).expect("URL should parse") +}); + +pub (super) const MIDJOURNEY_HOMEPAGE_URL_STR: &str = "https://www.midjourney.com/"; + +pub (super) static MIDJOURNEY_HOMEPAGE_URL: Lazy = Lazy::new(|| { + Url::parse(MIDJOURNEY_HOMEPAGE_URL_STR).expect("URL should parse") +}); + +pub (super) const MIDJOURNEY_LOGIN_URL_STR: &str = "https://www.midjourney.com/auth/signin"; + +pub (super) static MIDJOURNEY_LOGIN_URL: Lazy = Lazy::new(|| { + Url::parse(MIDJOURNEY_LOGIN_URL_STR).expect("URL should parse") +}); + +pub async fn open_midjourney_login_window( + app: &AppHandle, + app_data_root: &AppDataRoot, + sora_creds_manager: &SoraCredentialManager, +) -> AnyhowResult<()> { + if app.get_window(LOGIN_WINDOW_NAME).is_some() { + return Err(anyhow!("Login window already open")); + } + + let url = WebviewUrl::External(START_URL.clone()); + + let window = WebviewWindowBuilder::new(app, LOGIN_WINDOW_NAME, url) + //.user_agent(openai_sora_client::credentials::USER_AGENT) + .always_on_top(false) + .title("Login to Midjourney") + .center() + .resizable(true) + .visible(true) + .closable(true) + .min_inner_size(200.0, 800.0) + .focused(true) + .devtools(true) + .build()?; + + let webview = window.get_webview(LOGIN_WINDOW_NAME) + .ok_or_else(|| anyhow!("no webview found"))?; + + clear_all_webview_cookies(&webview)?; + + webview.navigate(MIDJOURNEY_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(MIDJOURNEY_LOGIN_URL.clone())?; + + let app_handle = app.clone(); + let app_data_root = app_data_root.clone(); + let sora_creds_manager = sora_creds_manager.clone(); + + let _ = tauri::async_runtime::spawn(async move { + sora_login_thread(app_handle, app_data_root, sora_creds_manager).await; + }); + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/services/mod.rs b/crates/desktop/artcraft/src/services/mod.rs index 8b92875ecb..45a23bf25b 100644 --- a/crates/desktop/artcraft/src/services/mod.rs +++ b/crates/desktop/artcraft/src/services/mod.rs @@ -1,3 +1,4 @@ pub mod fal; +pub mod midjourney; pub mod sora; pub mod storyteller; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/open_sora_login_window.rs b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/open_sora_login_window.rs index a82ecc4c19..19b75b7a02 100644 --- a/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/open_sora_login_window.rs +++ b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/open_sora_login_window.rs @@ -10,23 +10,23 @@ use std::time::Duration; use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; /// Name of the window -pub const LOGIN_WINDOW_NAME: &str = "login_window"; +pub (super) const LOGIN_WINDOW_NAME: &str = "login_window"; -pub const START_URL_STR: &str = "https://google.com"; +pub (super) const START_URL_STR: &str = "https://google.com"; -pub static START_URL: Lazy = Lazy::new(|| { +pub (super) static START_URL: Lazy = Lazy::new(|| { Url::parse(START_URL_STR).expect("URL should parse") }); -pub const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/"; +pub (super) const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/"; -pub static SORA_HOMEPAGE_URL : Lazy = Lazy::new(|| { +pub (super) static SORA_HOMEPAGE_URL : Lazy = Lazy::new(|| { Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse") }); -pub const SORA_LOGIN_URL_STR: &str = "https://chatgpt.com/auth/login?next=%2Fsora%2F"; +pub (super) const SORA_LOGIN_URL_STR: &str = "https://chatgpt.com/auth/login?next=%2Fsora%2F"; -pub static SORA_LOGIN_URL: Lazy = Lazy::new(|| { +pub (super) static SORA_LOGIN_URL: Lazy = Lazy::new(|| { Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse") }); diff --git a/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/AccountSettingsPane.tsx b/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/AccountSettingsPane.tsx index 46e2c6140a..b3d4185202 100644 --- a/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/AccountSettingsPane.tsx +++ b/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/AccountSettingsPane.tsx @@ -1,11 +1,9 @@ import { useState } from "react"; -import { Button } from "@storyteller/ui-button"; -import { Input } from "@storyteller/ui-input"; -import { SoraAccountButton } from "./SoraAccountButton"; import { SoraAccountBlock } from "./SoraAccountBlock"; import { ArtcraftAccountBlock } from "./ArtcraftAccountBlock"; import { FalApiKeyBlock } from "./FalApiKeyBlock"; import { OpenAIApiKeyBlock } from "./OpenAIKeyBlock"; +import { MidjourneyAccountBlock } from "./MidjourneyAccountBlock"; interface AccountInfo { username: string; @@ -58,6 +56,7 @@ export const AccountSettingsPane = ({ */} + diff --git a/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/MidjourneyAccountBlock.tsx b/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/MidjourneyAccountBlock.tsx new file mode 100644 index 0000000000..e54b3268ec --- /dev/null +++ b/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/MidjourneyAccountBlock.tsx @@ -0,0 +1,107 @@ +import { Button } from "@storyteller/ui-button"; +import { invoke } from "@tauri-apps/api/core"; + +export const MidjourneyAccountBlock = () => { + /* + const [midjourneySession, setMidjourneySession] = useState(undefined); + const [isCheckingMidjourneySession, setIsCheckingMidjourneySession] = useState(false); + + useSoraLoginListener(() => { + setIsCheckingMidjourneySession(true); + CheckSoraSession() + .then((result) => { + setMidjourneySession(result); + localStorage.setItem( + cacheKey, + JSON.stringify({ session: result, timestamp: Date.now() }) + ); + }) + .finally(() => setIsCheckingMidjourneySession(false)); + }); + + useEffect(() => { + const fetchSession = async () => { + setIsCheckingMidjourneySession(true); + try { + const cached = localStorage.getItem(cacheKey); + if (cached) { + const { session, timestamp } = JSON.parse(cached); + if (Date.now() - timestamp < cacheTtlMs) { + setMidjourneySession(session); + setIsCheckingMidjourneySession(false); + return; + } + } + const result = await CheckSoraSession(); + setMidjourneySession(result); + localStorage.setItem( + cacheKey, + JSON.stringify({ session: result, timestamp: Date.now() }) + ); + } catch (e) { + console.error("Error fetching Sora session", e); + setMidjourneySession(undefined); + } finally { + setIsCheckingMidjourneySession(false); + } + }; + fetchSession(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [cacheKey, cacheTtlMs]); + */ + + const handleMidjourneyButton = async () => { + //if (isCheckingMidjourneySession) return; + //if (midjourneySession?.state === SoraSessionState.Valid) { + // setIsCheckingMidjourneySession(true); + // await LogoutSoraSession(); + // setMidjourneySession({state: SoraSessionState.NotSetUp}); + // localStorage.removeItem(cacheKey); + // setIsCheckingMidjourneySession(false); + //} else { + // setIsCheckingMidjourneySession(true); + // try { + // await invoke("open_sora_login_command"); + // } catch (e) { + // } finally { + // setIsCheckingMidjourneySession(false); + // } + //} + try { + await invoke("open_midjourney_login_command"); + } catch (e) { + } finally { + } + }; + + return( +
+ Midjourney Account: +
{"email@domain.com (later)"}
+ +
+ ) +} \ No newline at end of file