mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 09:28:11 +00:00
login window + UI
This commit is contained in:
1 parent
94873f7d17
commit
674fc0d01e
15 files changed
+394
-11
No files matched your search
@@ -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<()> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,2 @@
|
||||
pub mod open_midjourney_login_command;
|
||||
pub mod clear_midjourney_credentials_command;
|
||||
+38
@@ -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<String, String> {
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod threads;
|
||||
pub mod state;
|
||||
pub mod commands;
|
||||
pub mod windows;
|
||||
Whitespace-only changes.
Whitespace-only changes.
+148
@@ -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<bool> {
|
||||
|
||||
/* 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<String> {
|
||||
let url = webview.url()?;
|
||||
let url_hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
Ok(url_hostname)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod open_midjourney_login_window;
|
||||
pub mod midjourney_login_window_thread;
|
||||
+78
@@ -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<Url> = 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<Url> = 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<Url> = 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(())
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod fal;
|
||||
pub mod midjourney;
|
||||
pub mod sora;
|
||||
pub mod storyteller;
|
||||
+7
-7
@@ -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<Url> = Lazy::new(|| {
|
||||
pub (super) static START_URL: Lazy<Url> = 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<Url> = Lazy::new(|| {
|
||||
pub (super) static SORA_HOMEPAGE_URL : Lazy<Url> = 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<Url> = Lazy::new(|| {
|
||||
pub (super) static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
|
||||
+2
-3
@@ -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 = ({
|
||||
*/}
|
||||
|
||||
<ArtcraftAccountBlock globalAccountLogoutCallback={globalAccountLogoutCallback}/>
|
||||
<MidjourneyAccountBlock />
|
||||
<SoraAccountBlock />
|
||||
<FalApiKeyBlock />
|
||||
<OpenAIApiKeyBlock/>
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export const MidjourneyAccountBlock = () => {
|
||||
/*
|
||||
const [midjourneySession, setMidjourneySession] = useState<CheckSoraSessionResult| undefined>(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(
|
||||
<div className="flex justify-between items-center">
|
||||
<span>Midjourney Account:</span>
|
||||
<pre>{"email@domain.com (later)"}</pre>
|
||||
<Button
|
||||
//variant={
|
||||
// midjourneySession?.state === SoraSessionState.Valid && !isCheckingMidjourneySession
|
||||
// ? "destructive"
|
||||
// : midjourneySession?.state === SoraSessionState.NotSetUp
|
||||
// ? "primary"
|
||||
// : "secondary"
|
||||
//}
|
||||
className="h-[30px]"
|
||||
onClick={handleMidjourneyButton}
|
||||
//disabled={isCheckingMidjourneySession}
|
||||
>
|
||||
{/*{isCheckingMidjourneySession ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin text-sm"
|
||||
/>
|
||||
) : midjourneySession?.state === SoraSessionState.Valid ? (
|
||||
"Disconnect"
|
||||
) : (
|
||||
"Connect"
|
||||
)}*/}
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user