mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-15 06:12:18 +00:00
fix sora / chatgpt login, enhance login flow (#586)
This commit is contained in:
1 parent
196e1c4615
commit
908ff43be3
29 files changed
+731
-335
No files matched your search
@@ -0,0 +1,85 @@
|
||||
use crate::commands::app_preferences::get_app_preferences_command::AppPreferencesPayload;
|
||||
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use once_cell::sync::Lazy;
|
||||
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::requests::list_media::list_media::list_media;
|
||||
use serde_derive::Serialize;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CheckSoraSessionCommand {
|
||||
pub state: SoraSessionState,
|
||||
pub maybe_account_email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SoraSessionState{
|
||||
#[serde(rename = "not_set_up")]
|
||||
NotSetUp,
|
||||
#[serde(rename = "expired_or_error")]
|
||||
ExpiredOrError,
|
||||
#[serde(rename = "valid")]
|
||||
Valid,
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_sora_session_command(
|
||||
app: AppHandle,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
) -> Result<CheckSoraSessionCommand, CheckSoraSessionCommand> {
|
||||
info!("check_sora_session_command called");
|
||||
|
||||
match do_check(&sora_creds_manager).await {
|
||||
Ok(state) => Ok(state),
|
||||
Err(err) => {
|
||||
error!("Error checking session state: {:?}", err);
|
||||
Err(CheckSoraSessionCommand {
|
||||
state: SoraSessionState::ExpiredOrError,
|
||||
maybe_account_email: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_check(
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
) -> AnyhowResult<CheckSoraSessionCommand> {
|
||||
|
||||
let mut creds = match sora_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
return Ok(CheckSoraSessionCommand {
|
||||
state: SoraSessionState::NotSetUp,
|
||||
maybe_account_email: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let upgraded = maybe_upgrade_or_renew_session(&mut creds).await?;
|
||||
|
||||
list_media(&creds).await?;
|
||||
|
||||
if upgraded {
|
||||
info!("Saving upgraded session to manager and disk...");
|
||||
sora_creds_manager.set_credentials(&creds)?;
|
||||
sora_creds_manager.persist_all_to_disk()?;
|
||||
}
|
||||
|
||||
let maybe_account_email = creds
|
||||
.jwt_bearer_token
|
||||
.map(|token| token.jwt_claims().email.clone());
|
||||
|
||||
Ok(CheckSoraSessionCommand {
|
||||
state: SoraSessionState::Valid,
|
||||
maybe_account_email,
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod check_sora_session_command;
|
||||
pub mod open_sora_login_command;
|
||||
pub mod sora_image_generation_command;
|
||||
pub mod sora_image_remix_command;
|
||||
@@ -1,31 +1,21 @@
|
||||
use crate::threads::sora_session_login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL};
|
||||
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowBuilder};
|
||||
use tokio::time::sleep_until;
|
||||
|
||||
pub const START_URL_STR: &str = "https://storyteller.ai/";
|
||||
|
||||
pub 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 static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse")
|
||||
});
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_sora_login_command(
|
||||
app: AppHandle,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
) -> Result<String, String> {
|
||||
info!("open_login_command called");
|
||||
|
||||
do_open_login(&app)
|
||||
do_open_login(&app, &app_data_root, &sora_creds_manager)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error opening login: {:?}", err);
|
||||
@@ -35,37 +25,14 @@ pub async fn open_sora_login_command(
|
||||
Ok("result".to_string())
|
||||
}
|
||||
|
||||
async fn do_open_login(app: &AppHandle) -> AnyhowResult<()> {
|
||||
async fn do_open_login(
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
info!("Building login window...");
|
||||
|
||||
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(true)
|
||||
.title("Login to OpenAI")
|
||||
.center()
|
||||
.resizable(true)
|
||||
.visible(true)
|
||||
.closable(true)
|
||||
.focused(true)
|
||||
.devtools(true)
|
||||
.build()?;
|
||||
|
||||
for (name, webview) in window.webviews() {
|
||||
if name != LOGIN_WINDOW_NAME {
|
||||
continue;
|
||||
}
|
||||
|
||||
clear_all_webview_cookies(&webview)?;
|
||||
|
||||
// NB: We're starting to get Cloudflare protection screens. Let's try to avoid.
|
||||
webview.navigate(SORA_HOMEPAGE_URL.clone())?;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
webview.navigate(SORA_LOGIN_URL.clone())?;
|
||||
break;
|
||||
}
|
||||
open_sora_login_window(app, app_data_root, sora_creds_manager).await?;
|
||||
|
||||
info!("Done.");
|
||||
Ok(())
|
||||
|
||||
@@ -3,16 +3,19 @@ pub mod events;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod utils;
|
||||
pub mod windows;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::commands::flip_image::flip_image;
|
||||
use crate::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
|
||||
use crate::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
|
||||
use crate::commands::flip_image::flip_image;
|
||||
use crate::commands::platform_info_command::platform_info_command;
|
||||
use crate::commands::sora::check_sora_session_command::check_sora_session_command;
|
||||
use crate::commands::sora::open_sora_login_command::open_sora_login_command;
|
||||
use crate::commands::sora::sora_image_generation_command::sora_image_generation_command;
|
||||
use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
|
||||
use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::main_window_size::MainWindowSize;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
@@ -20,13 +23,11 @@ use crate::state::sora::sora_task_queue::SoraTaskQueue;
|
||||
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::threads::discord_presence_thread::discord_presence_thread;
|
||||
use crate::threads::main_window_thread::main_window_thread::main_window_thread;
|
||||
use crate::threads::sora_session_login_thread::sora_session_login_thread;
|
||||
use crate::threads::sora_task_polling_thread::sora_task_polling_thread;
|
||||
use crate::utils::webview_unsafe::webview_unsafe_for_app;
|
||||
|
||||
use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
|
||||
use tauri_plugin_http;
|
||||
use tauri_plugin_dialog;
|
||||
use tauri_plugin_http;
|
||||
use tauri_plugin_log::Target;
|
||||
use tauri_plugin_log::TargetKind;
|
||||
|
||||
@@ -108,7 +109,6 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(sora_session_login_thread(app_2, app_data_root_2, sora_creds_manager_2));
|
||||
tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2));
|
||||
tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
|
||||
tauri::async_runtime::spawn(discord_presence_thread());
|
||||
@@ -120,13 +120,14 @@ pub fn run() {
|
||||
.manage(sora_creds_manager)
|
||||
.manage(sora_task_queue)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
check_sora_session_command,
|
||||
flip_image,
|
||||
get_app_preferences_command,
|
||||
update_app_preferences_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
sora_image_generation_command,
|
||||
sora_image_remix_command,
|
||||
update_app_preferences_command,
|
||||
])
|
||||
.run(tauri::generate_context!("tauri.conf.json"))
|
||||
.expect("error while running tauri application");
|
||||
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
use crate::state::app_startup_time::AppStartupTime;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::threads::sora_session_login_thread::LOGIN_WINDOW_NAME;
|
||||
use anyhow::anyhow;
|
||||
use chrono::TimeDelta;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod discord_presence_thread;
|
||||
pub mod main_window_thread;
|
||||
pub mod sora_session_login_thread;
|
||||
pub mod sora_task_polling_thread;
|
||||
@@ -1,178 +0,0 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::utils::cookies::sora_webview_cookies::get_all_sora_cookies_as_string;
|
||||
use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token;
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, NaiveDateTime, TimeDelta};
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
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 reqwest::Url;
|
||||
use std::fs;
|
||||
use std::fs::{read_to_string, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::ops::Sub;
|
||||
use tauri::{AppHandle, Manager, Webview};
|
||||
|
||||
pub const LOGIN_WINDOW_NAME: &str = "login_window";
|
||||
|
||||
pub const SORA_LOGIN_URL_STR: &str = "https://sora.com/auth/login?callback_path=/";
|
||||
|
||||
pub static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub const SORA_ROOT_URL_STR: &str = "https://sora.com/";
|
||||
|
||||
pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_ROOT_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub async fn sora_session_login_thread(
|
||||
app: AppHandle,
|
||||
app_data_root: AppDataRoot,
|
||||
sora_creds_manager: SoraCredentialManager
|
||||
) -> ! {
|
||||
loop {
|
||||
for (window_name, webview) in app.webviews() {
|
||||
if window_name == LOGIN_WINDOW_NAME {
|
||||
let result = check_login_window(
|
||||
&webview,
|
||||
&app_data_root,
|
||||
&sora_creds_manager,
|
||||
).await;
|
||||
if let Err(err) = result {
|
||||
error!("Error checking login window: {:?}", err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_login_window(
|
||||
webview: &Webview,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_credential_manager: &SoraCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
clear_browsing_data_on_test_domain(webview)?;
|
||||
//keep_on_task(webview)?;
|
||||
extract_cookies_to_file(webview, app_data_root, sora_credential_manager).await?;
|
||||
//initialize_sora_jwt_bearer_token(app_data_root).await?; // TODO: This only runs once. We need better management.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keep_on_task(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"auth.openai.com" => {},
|
||||
"openai.com" => {},
|
||||
"sora.com" => {},
|
||||
// Third party SSO
|
||||
"accounts.google.com" => {},
|
||||
"accounts.youtube.com" => {},
|
||||
"login.live.com" => {},
|
||||
"appleid.apple.com" => {},
|
||||
_ => {
|
||||
info!("Non login hostname: {}", hostname);
|
||||
webview.navigate(SORA_LOGIN_URL.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is just so we have a way to clear browsing data.
|
||||
fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"storyteller.ai" => {
|
||||
info!("Clearing all browsing data...");
|
||||
webview.clear_all_browsing_data()?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO(bt,2025-04-07): Heuristic to detect when logged in. Only write when logged in.
|
||||
async fn extract_cookies_to_file(
|
||||
webview: &Webview,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_credential_manager: &SoraCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
let new_cookies = get_all_sora_cookies_as_string(webview)?.trim().to_string();
|
||||
|
||||
if new_cookies.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let session_cookie_presence = has_session_cookie(&new_cookies)
|
||||
.unwrap_or_else(|err| {
|
||||
error!("Failed to check for session cookie: {:?}", err);
|
||||
SessionCookiePresence::MaybePresent
|
||||
});
|
||||
|
||||
info!("Session cookies are: {:?}", session_cookie_presence);
|
||||
|
||||
let (should_write_cookies, should_upgrade_session) = match session_cookie_presence {
|
||||
SessionCookiePresence::Present => (true, true),
|
||||
SessionCookiePresence::MaybePresent => (true, false),
|
||||
SessionCookiePresence::Absent => (false, false),
|
||||
};
|
||||
|
||||
if !should_write_cookies {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// TODO(bt): Race conditions ahead.
|
||||
|
||||
sora_credential_manager.clear_credentials()?;
|
||||
|
||||
let _r = fs::remove_file(app_data_root.get_sora_cookie_file_path());
|
||||
let _r = fs::remove_file(app_data_root.get_sora_bearer_token_file_path());
|
||||
let _r = fs::remove_file(app_data_root.get_sora_sentinel_file_path());
|
||||
|
||||
let mut new_credentials =
|
||||
SoraCredentialSet::initialize_with_just_cookies_str(&new_cookies);
|
||||
|
||||
if should_upgrade_session {
|
||||
let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?;
|
||||
}
|
||||
|
||||
sora_credential_manager.set_credentials(&new_credentials)?;
|
||||
sora_credential_manager.persist_all_to_disk()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_sora_cookies_from_disk(app_data_root: &AppDataRoot) -> Option<String> {
|
||||
let cookie_file = app_data_root.get_sora_cookie_file_path();
|
||||
if !cookie_file.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match read_to_string(cookie_file) {
|
||||
Ok(contents) => Some(contents.trim().to_string()),
|
||||
Err(err) => {
|
||||
warn!("Failed to read cookie file: {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod sora_webview_cookies;
|
||||
@@ -1,26 +0,0 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::Url;
|
||||
use tauri::Webview;
|
||||
use tauri::webview::Cookie;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
const SORA_ROOT_COOKIE_URL_STR: &str = "https://sora.com/";
|
||||
|
||||
static SORA_ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_ROOT_COOKIE_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub fn get_all_sora_cookies(webview: &Webview) -> AnyhowResult<Vec<Cookie>> {
|
||||
let cookies = webview.cookies_for_url(SORA_ROOT_COOKIE_URL.clone())?;
|
||||
Ok(cookies)
|
||||
}
|
||||
|
||||
pub fn get_all_sora_cookies_as_string(webview: &Webview) -> AnyhowResult<String> {
|
||||
let cookies = get_all_sora_cookies(webview)?;
|
||||
let cookie_string = cookies
|
||||
.iter()
|
||||
.map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
|
||||
.collect::<Vec<String>>()
|
||||
.join("; ");
|
||||
Ok(cookie_string)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod clear_all_webview_cookies;
|
||||
pub mod cookies;
|
||||
pub mod get_url_file_extension;
|
||||
pub mod image;
|
||||
pub mod large_file_transfer;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod sora_login_window;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
use anyhow::anyhow;
|
||||
use log::info;
|
||||
use tauri::Webview;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
// This is just so we have a way to clear browsing data while debugging / building.
|
||||
pub fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"storyteller.ai" => {
|
||||
info!("Clearing all browsing data...");
|
||||
webview.clear_all_browsing_data()?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use errors::AnyhowResult;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::Url;
|
||||
use tauri::webview::Cookie;
|
||||
use tauri::WebviewWindow;
|
||||
|
||||
const CHAT_GPT_ROOT_COOKIE_URL_STR: &str = "https://chatgpt.com";
|
||||
|
||||
static CHAT_GPT_ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(CHAT_GPT_ROOT_COOKIE_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub fn extract_sora_webview_cookies(webview: &WebviewWindow) -> AnyhowResult<String> {
|
||||
// NB(bt): Cookies were originally on sora.com, but now they're located on
|
||||
// the sora.chatgpt.com / *.chatgpt.com domains.
|
||||
let cookies = get_all_chatgpt_cookies(webview)?;
|
||||
let cookie_string = cookies
|
||||
.iter()
|
||||
.map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
|
||||
.collect::<Vec<String>>()
|
||||
.join("; ");
|
||||
Ok(cookie_string)
|
||||
}
|
||||
|
||||
fn get_all_chatgpt_cookies(webview: &WebviewWindow) -> AnyhowResult<Vec<Cookie>> {
|
||||
let cookies = webview.cookies_for_url(CHAT_GPT_ROOT_COOKIE_URL.clone())?;
|
||||
Ok(cookies)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use crate::windows::sora_login_window::open_sora_login_window::SORA_LOGIN_URL;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use tauri::Webview;
|
||||
|
||||
// NB: Probably best not to use this.
|
||||
pub fn keep_on_login_domains(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"auth.openai.com" => {},
|
||||
"openai.com" => {},
|
||||
"sora.com" => {},
|
||||
// Third party SSO
|
||||
"accounts.google.com" => {},
|
||||
"accounts.youtube.com" => {},
|
||||
"login.live.com" => {},
|
||||
"appleid.apple.com" => {},
|
||||
_ => {
|
||||
info!("Non login hostname: {}", hostname);
|
||||
webview.navigate(SORA_LOGIN_URL.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod clear_browsing_on_test_domain;
|
||||
pub mod extract_sora_webview_cookies;
|
||||
pub mod keep_on_login_domains;
|
||||
pub mod open_sora_login_window;
|
||||
pub mod sora_login_thread;
|
||||
@@ -0,0 +1,84 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
use crate::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 const LOGIN_WINDOW_NAME: &str = "login_window";
|
||||
|
||||
pub const START_URL_STR: &str = "https://google.com";
|
||||
|
||||
pub 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 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 static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
//pub const SORA_ROOT_URL_STR: &str = "https://sora.com/";
|
||||
//
|
||||
//pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
|
||||
// Url::parse(SORA_ROOT_URL_STR).expect("URL should parse")
|
||||
//});
|
||||
|
||||
pub async fn open_sora_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(true)
|
||||
.title("Login to OpenAI's Sora")
|
||||
.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(SORA_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(SORA_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(())
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token;
|
||||
use crate::windows::sora_login_window::extract_sora_webview_cookies::extract_sora_webview_cookies;
|
||||
use crate::windows::sora_login_window::open_sora_login_window::LOGIN_WINDOW_NAME;
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, NaiveDateTime, TimeDelta};
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
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 reqwest::Url;
|
||||
use std::fs;
|
||||
use std::fs::{read_to_string, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::ops::Sub;
|
||||
use tauri::{AppHandle, Manager, Webview, WebviewWindow};
|
||||
|
||||
pub async fn sora_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 sora login thread.");
|
||||
return; // NB: Only exit if we don't have the webview.
|
||||
}
|
||||
};
|
||||
|
||||
let result = check_login_window(
|
||||
&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(
|
||||
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()?;
|
||||
|
||||
let creds_dir = app_data_root.credentials_dir();
|
||||
|
||||
let _r = fs::remove_file(creds_dir.get_sora_cookie_file_path());
|
||||
let _r = fs::remove_file(creds_dir.get_sora_bearer_token_file_path());
|
||||
let _r = fs::remove_file(creds_dir.get_sora_sentinel_file_path());
|
||||
|
||||
sora_credential_manager.set_credentials(&new_credentials)?;
|
||||
sora_credential_manager.persist_all_to_disk()?;
|
||||
|
||||
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)
|
||||
}
|
||||
+2
-1
@@ -7,13 +7,14 @@ use chrono::{DateTime, TimeDelta, Utc};
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use std::ops::Sub;
|
||||
use crate::sora_error::SoraError;
|
||||
|
||||
const EXPIRATION_DEADLINE : TimeDelta = TimeDelta::hours(12);
|
||||
|
||||
/// Call this at session startup to generate the tokens for the first time.
|
||||
/// Call this periodically to refresh the JWT as it nears its expiration date.
|
||||
/// This will not refresh the sentinel token once it's been generated - we can use other flows to accomplish that.
|
||||
pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentialSet) -> AnyhowResult<bool> {
|
||||
pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentialSet) -> Result<bool, SoraError> {
|
||||
let mut refresh_jwt = !sora_credentials.jwt_bearer_token.is_some();
|
||||
let refresh_sentinel = !sora_credentials.sora_sentinel.is_some();
|
||||
|
||||
|
||||
@@ -1,69 +1,119 @@
|
||||
use crate::sora_error::SoraError;
|
||||
use crate::utils::classify_general_http_error::classify_general_http_error;
|
||||
use errors::AnyhowResult;
|
||||
use log::error;
|
||||
use log::{debug, error, info};
|
||||
use reqwest::Client;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
const SORA_BEARER_GENERATE_URL: &str = "https://sora.com/api/auth/session";
|
||||
const SORA_BEARER_GENERATE_URL: &str = "https://sora.chatgpt.com/api/auth/session";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SoraAuthResponse {
|
||||
pub user: SoraUser,
|
||||
pub expires: String,
|
||||
#[serde(rename = "accessToken")]
|
||||
pub access_token: String,
|
||||
#[serde(rename = "internalApiBase")]
|
||||
pub internal_api_base: Option<String>,
|
||||
pub user: SoraUser,
|
||||
pub expires: String,
|
||||
#[serde(rename = "accessToken")]
|
||||
pub access_token: String,
|
||||
#[serde(rename = "internalApiBase")]
|
||||
pub internal_api_base: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SoraUser {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub picture: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
#[serde(rename = "lastAuthorizationCheck")]
|
||||
pub last_authorization_check: Option<i64>,
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub picture: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
#[serde(rename = "lastAuthorizationCheck")]
|
||||
pub last_authorization_check: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn generate_bearer_with_cookie(cookie: &str) -> Result<String, SoraError> {
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(SORA_BEARER_GENERATE_URL)
|
||||
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0")
|
||||
.header("Accept", "*/*")
|
||||
.header("Accept-Encoding", "gzip, deflate, br")
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
.header("Cookie", cookie)
|
||||
.send()
|
||||
.await?;
|
||||
let response = client.get(SORA_BEARER_GENERATE_URL)
|
||||
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0")
|
||||
.header("Accept", "*/*")
|
||||
.header("Accept-Encoding", "gzip, deflate, br")
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
.header("Cookie", cookie)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
error!("Failed to generate bearer token: {}", response.status());
|
||||
let error = classify_general_http_error(response).await;
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let response_body = &response.text().await?;
|
||||
let auth_response: SoraAuthResponse = serde_json::from_str(&response_body)?;
|
||||
Ok(auth_response.access_token)
|
||||
if !response.status().is_success() {
|
||||
error!("Failed to generate bearer token: {}", response.status());
|
||||
let error = classify_general_http_error(response).await;
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
debug!("Bearer token generation response was 200.");
|
||||
|
||||
let response_body = response.text().await?;
|
||||
|
||||
debug!("Auth Response: {}", response_body);
|
||||
|
||||
if &response_body == "null" {
|
||||
error!("Failed to generate bearer token. Response was `null`.");
|
||||
return Err(SoraError::FailedToGenerateBearer)
|
||||
}
|
||||
|
||||
let auth_response: SoraAuthResponse = serde_json::from_str(&response_body)?;
|
||||
|
||||
Ok(auth_response.access_token)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Don't run in CI. Requires valid cookie
|
||||
async fn test_generate_bearer_with_cookie() {
|
||||
let cookie = "cf_clearance=nelyAZB.T1J.3ToyOuc_NozgPVqEmOoZXZgVqICRVLQ-1745296855-1.2.1.1-R..9Ca0ZCZjSoqDaWLD62phe1hUu.43nPpJV2zqxDibkFPCLJ2R2Am6ygUqJTB1_b86ctFGktBZ4YIPrHHow3jqPk5Mfu_VubL2A5r0R6R_1ILrdvY168CLSpAFRsJKIupMUPBFDePmQ738P7afr8W0YK6dO_m0qjYDYquZyCu5dC3u9CRUGaM8SLDZTTZkxoLzuxDzB1Skuns5rqdjTft4DoNUI_7EJvsYxKoklWylvL2FqcETULoyVq5FyElbZenz8VblXU4HLgspRTz3q0_SNh4_LAIxHygfXaCOBmmii1KY6mFHE83z4JYWgx7MM9jqxKKz1pHgVd8JR72aYgTo7WwE6ZpKmvuxhTtgobsA; oai-did=8ae351f6-bc99-45c9-9fff-bb8dd6eb128b; __Secure-authjs.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiYWNBYVdTWHBfaDZnNkxtUEs1S0RvTjR3NHFxTEsxVlpSTHA4VWdiaFdWNjctMG5pbDdVVjBCN2dSUU9Bem84ZXBjMVFjb1ozNGxwa0c1OEUyU2Q4cUEifQ..n6nMP5MA40XKktVazIps6w.tsgjkdkBaHt2jpO7GjIBzgammeanUH5W-3SQMPUDIG_cMrGcgmW_uaMwQY-LJ7qfWGfSqwZyGND6LQD_e-8vYdWbhE_gl1O8AQVJaLfCxJbpauCaXQTsSD0PD_XzGYsW0GZjWIbb-J8YFc-XKi8Y2AhjPhf2El5LJrvkQ4MbtlIJAhDOBNAp7UiBR4SWvWgtD60WqqnW3BhUjqXSigqhopUIkcsXAx_NHkfQpwFyJfkI8WK7JJoW4SbD6hdOX0BCKPjXKn7bTpYG---QkfjJ5f5wsRl0QYRVIQ4lzz4iak0A7oyV5jyX_aXYzK4iroVtEjUBAt77wTDOZ8ID3idkPOWQHPWnaugDzX9ZxZ8sWOxsttqbIMWWpEIm9G9ODc3-F29XHPLzciiLuRlRp2tn2ky12vXBdpiFINL7i5-OEryQ1xs1yEsSnAkzrGcXboXDgPSh-wPpmOhWLwqsydmflSahxQH-k0fUQAnjCq2EocdXnfIL84kqwnOxD36YGsKcX0DOir_3gL2PnAcE25Dql1OTwo9SBSLEf2c8dejDVfFT63UHmCkEqvHgdtTnZRApeqJj57A_2aFvy4d8mhflSaSD5qYmYUIdRo5I-uJumWOuHeDOwXOrlnCzxpgvcl1c5ng560I_oYgNUAhIGUnL5ezMQ3MD6jKwVqk-0kKZUPr2ebE1_w7jh4Rau6esLYiMcJI-Jfm_0TfSxlAXMKC3CKIII50PcXXOrvxy0yiBU_DwU79L6fYPAq7NFJ26P6821iOQGP-0mkaHPZHxEJGaPVKKjg2Y9ZQJ9u1gnm0GWwssY4uhFet6CxURnpqzIXKf78kevXOwkEsgFa-2hhMnr_OWBzKSH4UfVIymulxqoAoOh7Em51NUF-HJ0I0rr_3m7wBlhNvVVnmaxSYkuArZoboixX9ka-JVDopTPXUvoEykm45LRRvPdGiPB5hvCrglzNrp_vep4n-YbcLp4Uzn9ljcMjhSFZfXAzqquaiO7H5ys50_-mBquTq0Hoag2pHbVr5uJL8I1V-b8F1XWOnRtRIlj5pHoLX6KyxlT7pNTTSRgMo9PwAWxmLXpRba1h4ceKQTDputfrD1-itFJfZ3FZgkAXVQGrYyXEQIj79ERqq9xw18qGoBi9-GQkqBmCg1TxeS35SBnc62L7gh-UsZ_LSchizC4l4KODSeKdyNGHRONzK0cNHAGWqAJpLu5vBh_VxiljeaQxc4-p0SRiMXPYQf06uWDaRPdglCcavnlPKOcbL1GLku8CzMMCTAeqN3TJBV8BOHXsW81jw73oaBv18peTpgaJS6-Og9jMQVDb8pwlCi7SZnf1uafeGnBb6jJN4qCBN5EjZcoQikHxv0SOYmjs2XN7xE7EyczkovGcU2K3v03cnbbe01U_1mBAXgiJrW8gb4u3x8kngtphuWjEbStqHivLCtP5dyD22LTEMUE4ixvDmKwuLoB2NukM4QcB7PwrMnjI30SfCx0l5OVToz-TmLkLa-Ous-2cckw-D5pXD4RsgLv5NsxGQ9rtaL3qWeA8KnOByvYH1thLFtBUw6rjHtwLnQUGPomJkXIV8OuEuIWWnbsD1jW7hIpbYynhizTyrLChTnxuD8Witu01NtRa0dqSEhy3H5Y0qDXqFhaziydmW_kIdDxQBCWvPBNVBMAlXldqXgazM05eK3DFP86Div8b8k0pjXNoEtmEcwGLR06taiwQ3Fjbyc3y4HSLnQ-_yYz2jKLbDzpVJdaS6tlWT9rOrgniCkfvxJ5LE6KMF_2i6L_yFprOQ1DCYniM39CFehsD-7XVpS2Ui9t-Kx1g7eebK229kdQzB6Dm_HBEHTOxyXMiR4C0n24WYxEHukfCSiAvChiXHhfHOldmzusKY0r-MB_knKgM69elcXN_VAiukuUo1EdrsaqBprHqwmbkEWYfg-TmYefXJnf-58Xtx0lC2T7Uq8JJDdQL4f4PTnuyU_JX5NOwg9CwDUZoPE4gvYTicshEKxw4TABawLB6LXzr5vxjr1PK9lP1H7iwSyutBUGHEyK_R7fM0OzFQSfMEENwKtaGmGT5GnMTkvF4iGTyEtmZS1RhWqYyIagSFkf66ZRIwt5uohQJhZ8kpU_EOfihCkPC6c8he18W2TPtvGWxe0ljNfykMl-w4564IuHIYO3sESs9RzOdHkul9YqpjZC1C831zzEyM3vgM3hYWKo2VmovC5uMYZO0roqQyuy9RNbGPbXnbzMCE5tTPNeHMd1FVYNGy76t-BLj69MfJN0C-U9RgaLRe2Nz34EZBBfK3v9NNCLYhqm04WhM7XwX3CMrG9_j8mlMKpb_MxYqvQUodtttrBoe3bS4WId7OSz8k3KiswcZD86yopGq5X_a910-C3VzVfxo9jCLlRVP4moY2iohjc9asuQrKjqkUgEXp0bLy-gBB67UxV69rXMBEtz2pLYqfNKEUFFL7cVF1d407QUuSRpANitXUFbmtWo5M-0mJQM7LA0ktMDZUl3_-hPxNHFt9Sba4xDEiSyjPdMZDd--iNpCkJAGvManh-cNYI7HIBQM3kLE4GwD3ihStlW3IT5MfITEXfaaz0vWSvYdMXXmcVjVzgVn5JchynFilRsx7QK4JT7jPb_LQ1vAENBCfIuEERty8dgrKOvS4k21Br6IXckd9hl6l_5GoZUpbQzEY4BWGA8Ly8xKGbwwF4L-Mncc4TQt-2y09qYqU0TMTTXKBGgFfjVn4r0yIvryieCRAHSWuethGyYVX_Qhk_nZB3jqFkp42vMc1mkTjBCR-jlUCJWMbMcmL0tW2_SXDOk5JrACsq-T1djWeUCPgGYXCqYIygtSEE0f025feOpQJAynV78UM2w6fbgGBvsODpVr8Q2hjiI9Ydwh6MIJW1owo1QDDnuEOvTmdwdsQT9wX1H-SyN0cusSITAzgNMEp-NCj8NDW60kqTCbi7kMLF4OtPOK8THCBBkZ5c_SasAvSIxXo36fnMEKGPOLZs9GzLKMtKupym5PZ4H6H2zl_Y6iOIBniyQ3HBs5edLBBGQ3qqeeI_j7Xpwsr06cTCuQGYFAsNhu9dSs3LQTKlVfVeTiqBjRkxAiaQJgPUvFpOCvhIe2t-5n-MNj4.0dhenwDdC-iJ2W4uNnbx9YQcfEgnYSJzo1vXNawiifA; __Host-authjs.csrf-token=61aa1209933a1c967b7af6a787481f4e249a2d6b1d535924540addd4bd4ba196%7C510cca933fb90ce231290471f3885cdee2fdaf4f8f6994d96cfef9c63227e5f7; __Secure-authjs.callback-url=https%3A%2F%2Fsora.com; _cfuvid=XjJAS2zDerYymewq8Gl3e572OMiJ3cyyMqsmFiuGMn4-1745275079858-0.0.1.1-604800000; __cf_bm=hVGZ2X.67OhOZrqzUiW9Xg4IYGdCnss73d7GDU75YHk-1745296490-1.0.1.1-ppPUYZX7zAWYWXknUVzYPLjI7HDdBfmQz9DDMT5pMUNvqKBMhbUrJCp30PzxmBjMYwQL3injMe2IammgQNE5cj7JO7o6Alljt.MN1Pp8s8k; __cflb=0H28vtyuPgbsDEUozcoHSWoAnvpSqAh9friCA2BEchB; _dd_s=rum=2&id=b52d0910-2332-43c2-9fa0-da1c33d06227&created=1745296855240&expire=1745297781201";
|
||||
let result = generate_bearer_with_cookie(cookie).await;
|
||||
assert!(result.is_ok());
|
||||
println!("Bearer token: {}", result.unwrap());
|
||||
}
|
||||
// NB: Logged out 2025-05-06
|
||||
const LOGGED_OUT_COOKIE :&str = "__Host-next-auth.csrf-token=ef2192abb44aad120d2bd0c67ffa156661b3a971274bebd5822bdfb45d414c38%7C05f5966f7a8c00637388b2c135eded2502ff7af04f986513354123f2fac31a24; __Secure-next-auth.callback-url=https%3A%2F%2Fsora.com; _cfuvid=oD_JCwm5zaQGmsQIy4XjzEoGM276yori_BR0EOBEEFc-1746571084534-0.0.1.1-604800000; cf_clearance=jHTsbECz4hIp7a7acOilpM5iKqbRCOcCMIiy664dbPo-1746580754-1.2.1.1-xNvHlmDlSKGCbed79EyntnpCU7U.dpI9qyVYYEW0HuXT5YKQoWEepQVbB8wMEWB0MtXemxts5SfVHYhY67uESdyN7yIrEP2LPiQ0L1tTdsjFoOvsJGURGdqRAiUF8sjqCdJ1Gfx8FpGfbmJUUYh1I.wV3c5e4zcw2M7Xs2iW6wSA4kvmuwZDXj7H4oPgInwhp_XVXa.jKPHPnNIMRoGR7dirbYhY6wcywUk53Uw4j418ml2Bb_93LWlhzVO6WVPfjnKvWc7vxw1_cutXLy.IqClatx17HXzbCISOVgOcDeGITvvVgbl5OTmXbBlGXtmaJlxtfjsFbxHJFgX93zFYq7pt9znzbM4DPNSO57A0eP0; oai-did=feeb4ee9-615d-451b-91d6-35c9b9ca9826; oai-sc=0gAAAAABoGrUiAr4TfGAcKlXSjhT1h5_5UYzixku3R9aaTwvYNKk4lKP_Yk1OzPWjJL0J1CwOzC2Jb17t3aRDM3z8kxH1WOAZhIdnTU_u2NBgkfQNUQhzMBR_Qb5xHkfJ4PFUpwO9n7YNDtSoN8QVP7U2hbi0GJfMA06k0kcaceUmTvbEK6Uceq5R13QVhTPfZC9JdEjcgXorTcq7iQW9mQPJYfVhUwKgWbtdXgwQROAM51Zq4ETutVYZklPhaXy0hHs1LZdM0GRd; _cfuvid=tYpYCaVU3akRC7xqJ9HhgKb45yUMSDkSkk6zoY7wxbA-1746571091829-0.0.1.1-604800000; cf_clearance=oAW.3k9.ail0y7zwjDtsUlJIC0OAd5NllKMUq7r6T7o-1746571092-1.2.1.1-N0zV9i8bumZPfGDsB3TBpE5dIoU51SUzUvjsycWn8GmGHo6v_M6EeuCIEg9OIfAJEeJWNvE6mf6v0wOZ4x2FrUPBEc06NfnQgVb9fkIE5vzUsukkPX7jK1pigl7vtSiqDmFKyvcg4ojUUUEHuHPZeXDmiDE8UdHzLPSrMaANI6V7k5cflmlhnho8vgnFGpkiHfOArlfABVTkHWmTQo1pJAuQxhdSM3hMyxpA_6VHeQC_fUFX8Rk.dSwQzx7dCV_TCY5PKbOvfOd6X93sPSaQWneGo9hrDOTlZxeVEGLfpd7WngdKPS_fbjf6nDmwHnMtmKPMZsd_BLS.b3TfkmqngKPG_oZxWxi730a5NJv7O6w; __Secure-next-auth.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..UtJZGQCBDAEdkNRL.h7_eoTetqxFXGt6RmB8fr5JYaJPXH6vvcSMKG05JDGq5Nb3TSNXHfOIYdY5g_E-7xUgsPJnoE9RHmUZRuS1YCA0UW23cYmwHigGU89KKcdtJqg7rpcgosIQ2UTLY7aXe1WZBR8-3ACI-auo51JzMrjD7vnDL41k6UgGr7tdGoieyVxqlB58ughkcfIyneBm9k7VdSTahgD75BQRqWEtruPoAMCir5qtbu5bnDyAiCjT4S02gK-kVfgGlP5_tUTT_VoSQ-YMGE0Mw3Iae85l4X5PjuBj_3uhRlKEL6aABfs3KKr8gvsb6xGP_cypWVaTcPMpeFidLicJV4GTbj4x5ncGdJ4E9WK98j_fQRB6tQnF8op7UBuh1zOdKUFvmrKof4DgJm4ChPeff-kR_z64jRkZfiDdu7hTwEx1VIoIcXiVjBLg4MJAG_79WhETxiz61-HMrsUpNgRD3QSH6H6j4X3HpPs3hgiBZnd0L0WDOQU_mTcKBkvpm3Uubcatv5Gss797TelMs2cM7J64DDz88tMzChyMj1mBPguONIWlrj7G5pYkC9sLY5gyydZoK4cvfcxkAVgWa-UZJAtNwpf7XdYw9FutjW0Y5hucMqch3J2O51wVx6KltKjycLXGIJfFydq5qO53Ngu02WEz2AbBfHsdBqR2S-46t16LEqjCfWzJRuUOe0V_Ao4_lqVxgjc4Nd97pjaJhuHIFjWVY8GLmh5rqVUDAiDexEax7TLF8Htp6BBBdOt5-j772SbrvjCD8u06OyksGLIXElY66k4RMGsyzJLDxYoNa-c7E_38WtQJsPU2QH8kYRkwP8H_QYdYnOpukHVY-2Uv5AhAwaP2q9zqYAwPGyJ39mWQq0NrZ_rGqotRz9n_3W37Sbu1eX_5Ae0Vn33BuKG3fzxMU7akSVmA7_4DE_4dQC8PpmQ9C4UoLYhxg0aKCs4KVOWjpSTkLRoehiB-15R-SNHwNGKUMGoFg9B741jb8SHjgYsAVJnvj8VPPDhFm4r4rSqv-IIN--iebrj3ExC6YqBJmwWp-ZAvioSElJfvqZasoYkRKm0CjLoXuVawCcHVNngK0U0PjEFMKkveD9E4-MW2KO2uhud2OShZvXs4ge1Fvmk1yFwttmVM1f7MvSEoAolivtFj623iqlT-Sw0zTSg1Dw4-fDIfPk24r_eoaZvooP_r1sJ89NDDmPJDmZm_oavGdkP_2CCEEPaVNq8m72Mu2K2Le1geyvm8vepT9OR_5q3MDypRqs4KLruzrSqhfDr80Z0FMJHQGv-uyugY9yroX8GFtTBiXdIbRBiGquLgkOvjuw92JDTCT3HtMm5liFR4tGXbcsEKqp_HtK4cd1krKIphq71LKEhklkSXC-JvNzWS4qYEANdBrNfEaSa2e8-wtzW2qIuMfOZT4Cocv1H49cW7SIB1_5-DG3dy--1s0Wf6JniMn0Cprq9h01A1hraFxpSax3gAeOehYCLk3soPu1r0VlbXzdxTFg0S9AxeuXwj-IKL39C9gpQSkkeIsVeSL3VFlzYlpdupZcNbFLlp8JMoIlOnNaABBFPji12ln9dkeyEEkFq0JL4UyZLZ1SegADd0l7s_cn4dtYYfuIuAAJFC16vHDJ7UMVfVMhBeKA96U1w4ak-uKs8wk-l5EhGragNpPogCh_ccHyQf9zV3AnHbJyGsiVc1ztjNu6mduFB41FO3kQDBH2uoa2PgdgWtaO-zLSrTDns3aGffp6VWYddxuPvA3T5VQDVjVLz6eJ2_d2N0B9sPmevKkIYEaI1-OClY_StB4TG3VdNUsfcaqymweg43scS4DZBa9DPGz7ZmC-HG_z1oVfaoZGfa5-XD-u0Oyuu91xYM-QOuccvsHco2GWUTsf52qH51mLvkpc5AkaO9tUzhN0rQLabOGSUBjJZyxIFrRsLLlDWDwPSr3k7BbB1fBUvsUiPShqI_CWKJjfqsEiHjrt3MbfcbLc12XqYuvrCHwtpi0JRP67TwnUKADP7SEP0FEdXz34prhXpJSvK49hmpUkKYCbsdntJFz4J6vB7BKLcb9B6MsWNH5N14yhn35UJZc7_IToLLnjG93KcOhSWNvXtP9vTXRVmxIJtYfnZOOI6wsTxZ3WQcAypxYA1PLzJzlVrZC2TkT9Jhcph_Y2uHCKm0CFka2nr8DHF2VyrEo1n4x_bjXve_BZigWgGgFE0Vn4NEUdLislf0jTXcVhTwVCCxtYGPkKE3NTpu-Djl3HjPUGRYFC3dXa10HWJiazfOITTUO85LEpVRb9j2lhQaX9dKe6toPipSpGwzsdkOaw1liuADV9J8ssMSkhs0W_CLKd2HaJioPKUVUJ0v2B5Z39DiLyJ_NDx6vZoGzO8bRUp2Atm_z3ccrsaKuO-brZBmG_gApoXLQXdBPwovyS36TXx0fcdqcFlH5yqADtK1Yn9Zjkp5gIzbPz-k8nUtBtu4_JOayzFJpWGheTrkoj-nBIvuOfQB1yJan9BVZPF-KzALb1JdTC28k0kK60Exf7r2lulwA_3_-Ydg8inZ1HHGUYouAb6Na1r4Pk70g4waTzLItIbDPOeATPfvV9xRJ7LT8Yq-pbfIg1rHKRjrCcQ8IeonjziGxfNnFiPy4ANLtg2F4Z_dwi0BPE9MFo0yO7gKELGGpGoOB4rcd8mCK_wnm6MyBbdqC5oftova8ymTqjqNEH24L9DwCk8F_jx8DJhPvgrOnMU0Gc691Ym5uQeF_aMwtn6AaauBVr2Qf8VBE89wz1-sxk2vI4nJQasW9Q8wanqw_pSxwMoc1XzG8MY2sNRRREt1Ex1nmR4HjVdDSYJmbnqYgREPiJ6zUTcJkNJDPQjo70R1TEVjV8v0a97Ca_Fijp8CG2ZcD4VIAM8kOF90RJlUeG74pCIscB6AGi7tmgcO9a5VdHfeeqomRlFKVN9Ipi255SZHpI8JiUaUGwpP71RE-JHo4baZ-g7Z6i_UJsh6-76LKwL3HWblb2shAazc5cqC-ySHohWfY6_zeFD02zeoVN5vr-SaoQjzywVfef9V4AS5cs8BqlIJYP6Lrzh2AC9GwtSpEw22NVT6rR4VAIbER1HuGbwnNmnCDwHaOoyzFwkVGU8uohlOG8QYPUnLLfl8LzKv2O6SLlwcdZA1Fxiftv_ziMA4qo9Q3NQ-bblDycx-oKMWzSAvbx4LrjYBOxVn-XErq7B7K_i2kkK1C9Ttf63zbemz0aPcdhXWiEnKArv-q3kj5y21FKlmI4TwZuTKV5xlu-mOfXXWmTZZysOT6my6x
Line truncated
|
||||
|
||||
const OLD_COOKIE : &str = "__Host-authjs.csrf-token=aae0efad45e177963b2ec92343e42fb5edd2e61600c8d5f986caa1aae5a18a45%7C88e6407c124636cc2a5de4fc557c6fbf4cb77e6876cdc798427fe739a689570b; __Secure-authjs.callback-url=https%3A%2F%2Fsora.com%2F; __cf_bm=URhSdu4GaYtMgZXeQSJUHHPrp7ncob5oLSBZVYJqUBc-1743622054-1.0.1.1-ucpMZjhlQwqfwqRcYdDCrTrfN6O7qai6OU1Fr4iBg.j6asF2FoF5Ih0f6JETLsLZEkNmugCqLVAsh7tERScqIDckcN1NT9.ARPjozqgsjw.MEe7SwynVbQCw5TCWzSIj; __cflb=0H28vtyuPgbsDEUozcoHSWoAnvpSqAh9Qx421HSZDQR; _cfuvid=3fRsqhtWJn06LJpJsT5mBtjFDIy4Hu_IkUh.xGZWqhQ-1743621009366-0.0.1.1-604800000; cf_clearance=eVjck2N7zn0.mF9XP94m.x1GyyVSYgp_F.UKgaVO20c-1743621009-1.2.1.1-ivofMJGm9YOYnPABiwCxRNOwepOeiksOUI0pBj4rGGV94RDDUE5x1b_bDSKKtkW5L7PuTxRXOK5_yjZkXfjGhMfrXDeJhiXzIJGGN3p_4OAWj2Fp0fNhvV1rtALM7gd8QgTs9iBiWwA9GmUkZIdZ9._QhWx.v7cfuVKjFv2U239z3CzjvQ7RmrTDrWS2B1RCShW_m4RD6DY4QfJZSdp0NKQqcnzrc2nzBHf19SicEo13KU7OJ0htu2MyV9XY72WFXfXy0rUJGByRgB9ebpoFUA7is7ms1.5gZmUGHJ3xfdxwhACR16r4PV4eUBmUDpLNbsw4zrc7YADDJPcujclnyM13KAo426yPsA.N1NeK5TQ; oai-did=30ce2582-2117-4941-a891-c417e3ff316d; __Secure-authjs.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiYWNBYVdTWHBfaDZnNkxtUEs1S0RvTjR3NHFxTEsxVlpSTHA4VWdiaFdWNjctMG5pbDdVVjBCN2dSUU9Bem84ZXBjMVFjb1ozNGxwa0c1OEUyU2Q4cUEifQ..HCOVM3lniGpAz6nESjnKfA.nUde2F0QzNHHKj1pozvMKXvAd-YBSWrGW7k1TGGmyPYqDivgKxSZEcCYH2dvtPgBJ1M2dkc2RToDHdXHG_H1GL7QU-JPoRbf9EyfXfNf9KFTI8ymYT9sBvInIuyxzXCNp8fgWT96BjFjnWG-HK035XQhfsGp7ZvUlLdzTDibxo17xzALBYOKOwHGROGFXFZFxrA33m4_D14GAfxQ_YikvKHbnwEd6I2LeJhgrlrftn7XWTOvyzQwpoWQjSmVtvHW6el_wrvUG5ixZR_0IhMOK2lstQLQrqAv9VhC7W4v9OybqfHaSWZ_uwjjpkcP9QrOyPAeflcNKvIz_DtD_2VkoZM-ik82EZvycueEUZG0bBr6SX0T3AU71vQHQdqVrS7vL8As1rkJPAyBBLws5Os6xelij8BaG120lUmVeg0mRLyHWiD_hJk6cfKsvvpcV9wsqwaWs1PDB4HIIhYM5JMGg-LjtbMY6esFYIGibwGaQa1Ngcvp4JPW4vGIU1qzZnaYSPSdnDMPwDSYS4bZwWnYYi_7WptDbUTJ_wzG9jnUQe6fdUTZjFYBAOWjZo6ip3GeuSEZPEimlYWtCtwIsx42AkMneu7sIv2n40UnK4L5cPF-Hss9jvgm7qeuk6A9_kRS1x7owJ6H-byZiD3mIQ4UtTpOHHk97NK4lSpDyvAFIzh54xQzQiSXHcsRBC0VQ6UnLYm3CUv5d_ozv_A8o7klMUec7bG9LNIavqhEdvwxm-RuZ6VmiiDaOFQVjD4Nyid0WVjUOGjQqNJE_5nFhujw-Ob_HSGYZW0Zp1tZ6qlFV1QR3xUyo8jg7KJpng-qcNfNDTBr7AzwABZ7-3Iknw-dJIN0NaHhLYBBN36ZTNjwnySOpKsHssFWutYuqoUGiyW19deoEIBqfJ_d9SnAefTSbhKSsuxolvtdGO7wqFeq7pZdljmRnzC0dpoOLe8DPSYprnL84mDmZnCvanVObSRJW7R3F0Pf5hsWom5J8udvK8dNJhPwtrmPngnZv6Bd0-pEJhQw47qcX6j6emSTLEkAivVn2HknTMpwAoyMpairnOY4EZ3T9ETfGagvtRU7RcdMMwWH1NZ853lzkulHtMGF_ut2r83yKNi0M-ogcQVZTEU9F8QCkEWXHXO-x8aMMlG-axzkkZmpmrT9X_1YRJfQRQA18UyxdSIOPM1h3Ol6qMddA5SHF26p1u4BxE82-ZD1kDlilGNzr3El1ds4jQryQsWWBV8b6wKz7332CSTE3lQJLcoGTvLhBjFgSQv4KgT6bVEFzzBXgP8B7YNzkXTZL-wTHKi9S8SjkTDxASLNmjigCSlf15O8hEM9VlGkO-vF3D3E7uTl_ZSevyF5SJdjgYRL-Q3ll4lYBDQU9bi-tpsm9RgJ8CnH_jAMTiwNdzHvokqGoiNqQX6e5s2KSGMS45j41iXZ-A3MwahWyjWgzAdszv-uETxPGqndWXVoAWcj6NXQkPPufPolFkkDHJwL_Xu6Ol5AURoNE7p0qy3dCDtk621uruLOJMfV56kRR6cS5LZ02mc75Dnjv3zCao8qWt1rmPdHlAiRW7JiPbJV__Whkm1bBxJl2zkhJquKshu4ViUQCkkt0OEMnzp-ZRCSYhZczXlGGczD1iA3BzLeAx4OO0baPWwYxb_7_B0sN4hL4L2zOqBHu8aUyyV1C8xJJIVAefWxbppUy8sGPvL9mVZjDolJ8MZjzCJMM2Zr8si8TF9nfrL9AT3wlFUU04GhknBAbGppBYKnRC7-uu5Z5MLZSD19xxNCOrwcPwPw85_qCz8h40NJSlkicTqxQ7yjkV0x_qDjIUgqfUISFt1W5Dmk2J6zxWLpL2cgFRD8LzDgSnNPtvgCXia0rOTu28aIstp3QyPtIUplwgbyHZxAw6M_HBjoS6V16l0qMdSTVjhbIBMI_piKVXDrm_8Pcx0PGa2Z7NSGZITXPddQLukShgraxsSUQtOG8nLmULnTqBKVLUgK3_VYGOEEMkfixPnmiSEuWmEg66P4_2wi1MceFPlhzxmUWthRUygZ0u9iRzAVUvnsFJ5t892_4QwhYlMlGQTESkCyeHNNIUhEHWl1eGDTRsOAk0cL5EnZFAR_jZHxap37zvn4UV-M0XyKcWluYLWGl9A6mXBpFujt8x5VfWaX5MJugeOIlvBL-AwDaqhZ1ekIWJ6AWDKVCJoP-Y8tatndqaSRYxPs8KMmdEkkMtLgSkeF4GHe6KxGqCYk9gCDdoUD-9Qrte3p71lC8qtb28FlomS4cq0zTt7YYor7DSvA1RIUCcKo90s3rmfyIs7riAviDaBFMGtwKhrkMY0LR5AX0LFFyNncGHgWNhPDHl7KDcKvFfSF3Zs9VcSEdfPOZdoGV0Bm28-Md_gtNEtEDdI52-y2cQmpWcaIi0CNM-ebnNvRXbeq-KjZeCPgDwyAbqEjBbbj27o69sQcSzHF-GWw12s5Qc6gx9y-tZ97Xxc1JadcHNy2MXqFX8LcWMKdgQJJJZzch1FXqZFgqOBQbTPvMnFZZDJ-vIAHvAavn_SE-pj98FUghAeF_Mu8xBW6aBvkz1iJrQkP1RREF_oIq0hy_2fVIY1LSrlwre8lCnwgjSqgPIpwAKpl2FiG0YeU.JTV0M3rYnJJEs3ETueQc5lbJu6yfercLCVcsFpuhTQo; _dd_s=rum=2&id=e4297d4e-7494-44e7-b1bc-44c9c4472d8f&created=1743621072209&expire=1743623584230";
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Don't run in CI. Requires valid cookie
|
||||
async fn test_generate_bearer_with_cookie() {
|
||||
let cookie = "oai-did=bb449d6b-e6d1-40c8-b5eb-972b110a0918; __cf_bm=JqXYK3aRe_0KZb8vsSijTm2YJvpDV33oyEQpfcKKKKI-1746570472-1.0.1.1-eMi1cn_XDBxJ2u_UqGql0BS5lC5FXwSxZCYjq2FR_AL38LBllAuKbWS93THhE5wjn3YwGhifW2fa0pAFEHIV3eZIZuH5oFSb.aWOqmtYEsw; _cfuvid=KbIJVncxt0WZo2fG_tci8JjgnxJBfcL3SxlLeP9OHE0-1746570472074-0.0.1.1-604800000; cf_clearance=mlWui.52Enn_gRDTISC4Lx8j.UcsRlKo2dfFd15bzKw-1746570472-1.2.1.1-MblVM7BHYxZoTmR15yY_dEyQhQJQgA4boTjDv25pG7MqKAOKh2QwH_8QHG2er0mTxrOfzOkKFwYpKD.DPV7fPp0fUIGVmoi.WwDVChrwdsH8kgnMP3g6w19TT5qmgHc9gMmpjsoop0mRjPbmPEY0lrKvobTvXAclTdPU87dBkQgaMQbFb5M4qDSi3XN2r.0aYhbTLdN0ypR3mx9Jl3Oil5HThUGNSy8rhqDis3TwtaoWu7GoeaXd16ubHX6vmEMnP1UZJUupE70WcfxSSGDMRcpkjePOjsHpbTuxDGziEdHHwODhQlrriXCp_LTZuCwiOPWNyiJSHEbZSFieW5s02S3J4smBp6qpSH_MMQgAh.0; __Secure-next-auth.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..DhbnG188knzLnuC5.yyYzvDaawyv5penH9KFwivcPeEuVX1xkaIAKGhT0aeW-e2B1lqVK8uiHmOpba6RNsBuJSrJ3zGljBrCwGeeD_vCLye53SH_1wqUHg3BrsPbGFozslPHr6HBDM2RF4fMYb-wNxuHoCb3QcRfZWGQbaxwoOlL3MlUt_ZZ90kH-_Kut3tGhWM82d7x2YSJiV0Bgci9AjkNPNqKQtod9q1_MCmW7E0Hos-09CR__sGp6GUBsdMHR64V8zDbEpg6nFU0--6p7kFoJ3FidbXGyiMlqeMi_rrr3Od03S2tbUJSTUguxA0y8SnwpPZ9oliQ1Gort-EbRRnUYG5VvyxMptiJXmhnz0AJ6Ey8d4BY9WtnMo9HcOm1wUF_7tZ14oTZVdtn6oFIQvo9xSa4aoV19XlvMCifYyYnqtOS5CYq5xzmu3BUJvfzsBIafKhAaRlQg8uyI9zaYZ3JpXx0Aa_8MGuUcOuDPksoaFRHYiy_UoPPzDDJBCGd9ymiBS9CE5TsXbBPE01IkvF32nWAaC3_HDUp_qEpksP5WNA29akLUFBjYL1WXU2aeGmDHSeDqwwI_2kzzga6TPibj1HDRXxfuJPf58cYtZ0jIjLYAA77LgQ59QGInRQRr6rN2TlbEf89Onkv0NWRBqXzZMu3qr0cj17m1Smq7HaLCgcRJSGdGR2LVKZpcZl2V0EuktLQumVgmZLSvcKqpyCk-zZkGRtLBqMpnsSGa4uC8wonoejGt8y6TQSdjPoTTWDynjEH48bFF_aXLIxqF4W_DzE5M7ymQ31PdxKQ1Rkxq9A2v_igOAAb1uN5oE8TGcFGM1aWpuqX6fEKsNMS-hUitTq6aase64uSEXUWWTMFKwSNhJCGOIo1p3zsfznjAxSYsEUYy0LiVPv7SpiDCcYsIsFxZ0QksS9i3CcuFKCqWPRNkSR_F0B2juQEiAVTHBD7ugbg0j8D2y68xGNMR-HrNBHld3k1LrA3pkz1m28dcllhL-NhZ9Izm98kvY1Sj3uGP97d9_Q7QG0ce_93xXPDSKhrV_eTV_BKkUSNN4uUBHm2kYiHK3_SOfNUlFHEGa7ANiiNhOwWGzj_lAeAbN9Ab6FEPvgyPQS0BEXDRu5tey-c2KVPZJjOWReS_ZUBXEy7fNCRAH5zsO1EeD3cjlv2MIH-Gupp4YqeZWW1QJPvFIlHqTBLvturXx9lteYxweeWH1wWedr-jbJfjp1csSF5E46WuW6MjfNPWNBYCiBg_Al3876mrAvag62bRgptUFpZ2m_VWVdENA_rhyL5ruyyIoKF6war6_y0oGlpPY_AULFeK0-thWtt2zB_kGgNt8HblwnskOvta1k1xZt9oXTMG9dlnlVLn-155VEbWnMF8wtFYcT7By_Fpu_BfCVyjdg70qk8YoibUnk0Jv7L2GHfmlPejQlhfX_xZiZ9zjC80Gs-jnQyV5xG_7IAUqYZ6BrCzGXyk0ZVLKC-gGWAN_ly6N5Rs1G-X2bEGRmE-Kv8JcvSyK3ye3T02ymOzIml0qt37RGNzOLry0cPi0GVr1boeYAJmJdBpT9w5PvuoXmnoMF5yhIVfA3jIxnNjJ7vkUaLOsKT13KOKtpEA0nuUALyJ4UmIQLZtD0j8xXgMsBolDTnt5LRbS43PrvfhtKm9l0rnBJ2Ruja-gOhUmCogwitZlfmrwJGa-2hf225nMr2L0u6biBaETWFwOQ-EEseOENEhqTbygTmdDMwl7iMbFbn4bgtj3N9tpfO-mua_e0oNWInEYvrT93qIKl7UIZegoxqWhN9cMbjsHd2I5PJF-ES96tfuVDYgoo0wQ5ZY61ZbNOh04lqy7TDYjLJ-YfXy01rU5z8X3O0sS1lg636fJbpJymYhrrCMpzfNMaw3veRdTTUzcLnLfakAlB1Rv7ouLX3XJ76cUO1PezX2ukTNDaMz5ls5b8OTFh7XnOTYT08nKHZOXj7w3_udrgPvLr7YZAf17N6BosQ6FZu-AyBtIZtZwaZR24JrBypo0Ny0pc2erMjEn35q2UgB1vgyhLADfvV2qzZwNSw2ziwFyMKdw_XE5ZgapKW-x3kEwhQJw2-B1sJcJqg4uf4GZcDUE5odvDAMtbziTX6b43VM6nVUlCTRXNznwt-NQ-kWoCSEna90AtJNgBbBzZ0BXPVNyps9B7lwNF9iQukF063pVEzwReYBjlqkSMxTSU54yN1TLWdhYqgg_4BAQsXsCdZw9PwY5n8vk3j6JI4LSiFPHUI_Lok3hNHK0gV4wdOgdoLTing4iHWRyq12ZVyS3p0PeCKNKLP1ZMePG6pW90GlBo1QdL3AbukJOITy-n102UFa1udb0GHtOfjQVzDGQ3hhUmka04H0-LexyfEi1Ee0HijvQFbMaWF4-Uzpzlu4__u0PDD9Kzxhw66NGFMaDxKxAYXiXEcsmxH_ptQPHOz_i5A5mNeKCFoeSl898T0mxPg4DRwB-R7aRzHgnUiputdJ2x5TAsQsMWB4XqfAVJqEe0ORNXvzezRPKiL1gCif1tyQj6acm1Hull78zPYvzNC76p3srFW2Ohg7W3tM0ODuLLx7E5tnmzAhnIWy_fkdcJIV8gvommNkBzaGWut5c_h_f6sP4WgIAgBi6Z60YSjMaflD6bRQumHcZl1dMi8Gv7j5BDPo7-phJIqpLc1uYzxMZ-IoAcSCSvmhh8cwsqONDj9muvflfYAISA8UL2ZVlRnHI_l6E0I8RXiXWt59u_UeAv9FdRtcmoQ85wa5axtUaJ19RkpmPygiH-A3xNB0EhwfOgI6OOrJL8L7EWDVJhViSYYCGVW5ZVOGsYyT7wKHqIxbrM18TJkJ4XJuHC9rUowmWLkq2xkbfmRpUQIMrvyB8HBaWmkTA5rZnqkYJDr4Avm4SOltFX1g4Dv-ediVZRtCMPYz37olezdDfBqPchQfYWbp4Bnd5Uy_Z6Rs3ciF2-Z0HUpF2i6FxezCIR7r60QRoWah84PS_2-_7hfhQyWUoQEuqwntI9y2jp77bWULIaU81g0r66_Q8KSSdwnxmIFk0OmhQCUN4Bsh63UDyGQ0AKII29QXY7lQU8E0kAyfyL2kTthsIdo4r6ilMQU9aZVJUC2Om7wAfQjZBYzyWNHdphyboGmSb5J2uKxPhE-whc87ZSfJ2aiQ4FiWu112SpjZSQ1pmrM7NDUtsKYZeUvZ85LoSKmgwz7oy2PaW5q786s_nAKVDAHTy5JeZbtWN3pI8CGA9kJakz1axsTBis_W8aJ3kRUyxgUFAJZ4C0HukYxzr8kvL4e5gjKF0-EvzWmN7TIWV03kzbh1-vg7X_gUsijHL-SCDaZMKqOz146zVIIpS-xNjr-7lBfS-AxciI6JPlncTgLntTxfuoP2QavSBc4PZ_Fbn_Ko1UxvQQhJgWYkoBH2hUpIvObn_BaeT2PLbVcht4GhUgjwkDxpV14u1RLDqyqOyXRyVqTVO3wDtmSefrMRWze0Q2D-XW5JUAHOjvuU-AQ4MdIV9LiVlQAOjlIx5e56mVWXshjmcCNaG1vHo62yLZj5Iz0kf7SjQZ-WZEzDbpPXUeAEsCANQDB92l-sRUVTdg.DlCfPqTSppBQoodvYnNh7w; oai-sc=0gAAAAABoGoz4VyDlu57UUFNU7plsIHZekd0cT9h36pzM5uCs1hG7R9NaGclCm7hpLmaftSFPPd8EMNOHKFfjF2XMwpdhCCtYVv8N9FQUwCoC49u7I5rS6ozwDiBur0tlCduiWNhx8N2w4onMd3F1EOJpVLheW9XNY23elJ2-3ZcX-3WFvdPIrQdpgI0PRjXuEyPNcGF3IBOsELHuy5EqCww2SibjLF1yFDtua06RUtACm4J9ma1pHGTzCqVUo91FCQi4F8ktbkVQ; __Host-next-auth.csrf-token=96efb8da024d1edf44c144394fac947b9350a05dbbe1fb7c62e72a0e1198b078%7C8cdcb6b83d18d8c4b0bf75b19e9178303fd51b89bbc543a1bbc875ce7bf1cf89; __Secure-next-auth.callback-url=https%3A%2F%2Fsora.chatgpt.com%2F; __cflb=0H28vzvP5FJafnkHxj4Jor5o3J
Line truncated
|
||||
let result = generate_bearer_with_cookie(cookie).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
println!("Bearer token: {}", result.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Don't run in CI. Requires valid cookie
|
||||
async fn logged_out_cookie_failure() {
|
||||
let result = generate_bearer_with_cookie(LOGGED_OUT_COOKIE).await;
|
||||
|
||||
println!("Results: {:?}", result);
|
||||
|
||||
assert!(result.is_ok());
|
||||
println!("Bearer token: {}", result.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Don't run in CI. Requires valid cookie
|
||||
async fn old_cookie_failure() {
|
||||
let result = generate_bearer_with_cookie(LOGGED_OUT_COOKIE).await;
|
||||
|
||||
println!("Results: {:?}", result);
|
||||
|
||||
assert!(result.is_ok());
|
||||
println!("Bearer token: {}", result.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Don't run in CI. Requires valid cookie
|
||||
async fn invalid_cookie_failure() {
|
||||
let result = generate_bearer_with_cookie("").await;
|
||||
|
||||
println!("Results: {:?}", result);
|
||||
|
||||
assert!(result.is_ok());
|
||||
println!("Bearer token: {}", result.unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::requests::upload::upload_media_http_request::SoraMediaUploadResponse;
|
||||
use crate::sora_error::SoraError;
|
||||
use crate::utils::classify_general_http_error::classify_general_http_error;
|
||||
use log::{error, info};
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{Client, Url};
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
const SORA_MEDIA_LIST_URL: &str = "https://sora.chatgpt.com/backend/video_gen?limit=50";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListMediaResponse {
|
||||
pub task_responses: Vec<TaskResponse>,
|
||||
pub last_id: Option<String>,
|
||||
pub has_more: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TaskResponse {
|
||||
/// Task id (eg. task_{foo})
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub user: String,
|
||||
pub created_at: String,
|
||||
pub status: String,
|
||||
/// Text prompt that generated the image
|
||||
pub prompt: String,
|
||||
pub r#type: String,
|
||||
pub height: u32,
|
||||
pub width: u32,
|
||||
pub operation: String,
|
||||
|
||||
// There are many more fields [...]
|
||||
}
|
||||
|
||||
/// Note: this request only requires a valid bearer token, and it doesn't require a cookie payload at all!
|
||||
pub async fn list_media(credentials: &SoraCredentialSet) -> Result<ListMediaResponse, SoraError> {
|
||||
let auth_header = credentials.jwt_bearer_token
|
||||
.as_ref()
|
||||
.map(|bearer| bearer.to_authorization_header_value())
|
||||
.ok_or_else(|| SoraError::NoBearerTokenAvailable)?;
|
||||
|
||||
let cookie = credentials.cookies.to_string();
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(SORA_MEDIA_LIST_URL)
|
||||
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0")
|
||||
.header("Accept", "*/*")
|
||||
.header("Accept-Encoding", "gzip, deflate, br")
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
.header("Cookie", &cookie)
|
||||
.header("Authorization", &auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
error!("Failed to fetch media list: {}", response.status());
|
||||
let error = classify_general_http_error(response).await;
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
info!("Successfully generated bearer token.");
|
||||
|
||||
// Parse response
|
||||
let typed_response = response.json::<ListMediaResponse>().await?;
|
||||
Ok(typed_response)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Don't run in CI. Requires valid cookie
|
||||
async fn test_list_media() -> AnyhowResult<()> {
|
||||
//let cookie = "__Host-next-auth.csrf-token=ef2192abb44aad120d2bd0c67ffa156661b3a971274bebd5822bdfb45d414c38%7C05f5966f7a8c00637388b2c135eded2502ff7af04f986513354123f2fac31a24; __Secure-next-auth.callback-url=https%3A%2F%2Fsora.com; _cfuvid=oD_JCwm5zaQGmsQIy4XjzEoGM276yori_BR0EOBEEFc-1746571084534-0.0.1.1-604800000; cf_clearance=jHTsbECz4hIp7a7acOilpM5iKqbRCOcCMIiy664dbPo-1746580754-1.2.1.1-xNvHlmDlSKGCbed79EyntnpCU7U.dpI9qyVYYEW0HuXT5YKQoWEepQVbB8wMEWB0MtXemxts5SfVHYhY67uESdyN7yIrEP2LPiQ0L1tTdsjFoOvsJGURGdqRAiUF8sjqCdJ1Gfx8FpGfbmJUUYh1I.wV3c5e4zcw2M7Xs2iW6wSA4kvmuwZDXj7H4oPgInwhp_XVXa.jKPHPnNIMRoGR7dirbYhY6wcywUk53Uw4j418ml2Bb_93LWlhzVO6WVPfjnKvWc7vxw1_cutXLy.IqClatx17HXzbCISOVgOcDeGITvvVgbl5OTmXbBlGXtmaJlxtfjsFbxHJFgX93zFYq7pt9znzbM4DPNSO57A0eP0; oai-did=feeb4ee9-615d-451b-91d6-35c9b9ca9826; oai-sc=0gAAAAABoGrUiAr4TfGAcKlXSjhT1h5_5UYzixku3R9aaTwvYNKk4lKP_Yk1OzPWjJL0J1CwOzC2Jb17t3aRDM3z8kxH1WOAZhIdnTU_u2NBgkfQNUQhzMBR_Qb5xHkfJ4PFUpwO9n7YNDtSoN8QVP7U2hbi0GJfMA06k0kcaceUmTvbEK6Uceq5R13QVhTPfZC9JdEjcgXorTcq7iQW9mQPJYfVhUwKgWbtdXgwQROAM51Zq4ETutVYZklPhaXy0hHs1LZdM0GRd; _cfuvid=tYpYCaVU3akRC7xqJ9HhgKb45yUMSDkSkk6zoY7wxbA-1746571091829-0.0.1.1-604800000; cf_clearance=oAW.3k9.ail0y7zwjDtsUlJIC0OAd5NllKMUq7r6T7o-1746571092-1.2.1.1-N0zV9i8bumZPfGDsB3TBpE5dIoU51SUzUvjsycWn8GmGHo6v_M6EeuCIEg9OIfAJEeJWNvE6mf6v0wOZ4x2FrUPBEc06NfnQgVb9fkIE5vzUsukkPX7jK1pigl7vtSiqDmFKyvcg4ojUUUEHuHPZeXDmiDE8UdHzLPSrMaANI6V7k5cflmlhnho8vgnFGpkiHfOArlfABVTkHWmTQo1pJAuQxhdSM3hMyxpA_6VHeQC_fUFX8Rk.dSwQzx7dCV_TCY5PKbOvfOd6X93sPSaQWneGo9hrDOTlZxeVEGLfpd7WngdKPS_fbjf6nDmwHnMtmKPMZsd_BLS.b3TfkmqngKPG_oZxWxi730a5NJv7O6w; __Secure-next-auth.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..UtJZGQCBDAEdkNRL.h7_eoTetqxFXGt6RmB8fr5JYaJPXH6vvcSMKG05JDGq5Nb3TSNXHfOIYdY5g_E-7xUgsPJnoE9RHmUZRuS1YCA0UW23cYmwHigGU89KKcdtJqg7rpcgosIQ2UTLY7aXe1WZBR8-3ACI-auo51JzMrjD7vnDL41k6UgGr7tdGoieyVxqlB58ughkcfIyneBm9k7VdSTahgD75BQRqWEtruPoAMCir5qtbu5bnDyAiCjT4S02gK-kVfgGlP5_tUTT_VoSQ-YMGE0Mw3Iae85l4X5PjuBj_3uhRlKEL6aABfs3KKr8gvsb6xGP_cypWVaTcPMpeFidLicJV4GTbj4x5ncGdJ4E9WK98j_fQRB6tQnF8op7UBuh1zOdKUFvmrKof4DgJm4ChPeff-kR_z64jRkZfiDdu7hTwEx1VIoIcXiVjBLg4MJAG_79WhETxiz61-HMrsUpNgRD3QSH6H6j4X3HpPs3hgiBZnd0L0WDOQU_mTcKBkvpm3Uubcatv5Gss797TelMs2cM7J64DDz88tMzChyMj1mBPguONIWlrj7G5pYkC9sLY5gyydZoK4cvfcxkAVgWa-UZJAtNwpf7XdYw9FutjW0Y5hucMqch3J2O51wVx6KltKjycLXGIJfFydq5qO53Ngu02WEz2AbBfHsdBqR2S-46t16LEqjCfWzJRuUOe0V_Ao4_lqVxgjc4Nd97pjaJhuHIFjWVY8GLmh5rqVUDAiDexEax7TLF8Htp6BBBdOt5-j772SbrvjCD8u06OyksGLIXElY66k4RMGsyzJLDxYoNa-c7E_38WtQJsPU2QH8kYRkwP8H_QYdYnOpukHVY-2Uv5AhAwaP2q9zqYAwPGyJ39mWQq0NrZ_rGqotRz9n_3W37Sbu1eX_5Ae0Vn33BuKG3fzxMU7akSVmA7_4DE_4dQC8PpmQ9C4UoLYhxg0aKCs4KVOWjpSTkLRoehiB-15R-SNHwNGKUMGoFg9B741jb8SHjgYsAVJnvj8VPPDhFm4r4rSqv-IIN--iebrj3ExC6YqBJmwWp-ZAvioSElJfvqZasoYkRKm0CjLoXuVawCcHVNngK0U0PjEFMKkveD9E4-MW2KO2uhud2OShZvXs4ge1Fvmk1yFwttmVM1f7MvSEoAolivtFj623iqlT-Sw0zTSg1Dw4-fDIfPk24r_eoaZvooP_r1sJ89NDDmPJDmZm_oavGdkP_2CCEEPaVNq8m72Mu2K2Le1geyvm8vepT9OR_5q3MDypRqs4KLruzrSqhfDr80Z0FMJHQGv-uyugY9yroX8GFtTBiXdIbRBiGquLgkOvjuw92JDTCT3HtMm5liFR4tGXbcsEKqp_HtK4cd1krKIphq71LKEhklkSXC-JvNzWS4qYEANdBrNfEaSa2e8-wtzW2qIuMfOZT4Cocv1H49cW7SIB1_5-DG3dy--1s0Wf6JniMn0Cprq9h01A1hraFxpSax3gAeOehYCLk3soPu1r0VlbXzdxTFg0S9AxeuXwj-IKL39C9gpQSkkeIsVeSL3VFlzYlpdupZcNbFLlp8JMoIlOnNaABBFPji12ln9dkeyEEkFq0JL4UyZLZ1SegADd0l7s_cn4dtYYfuIuAAJFC16vHDJ7UMVfVMhBeKA96U1w4ak-uKs8wk-l5EhGragNpPogCh_ccHyQf9zV3AnHbJyGsiVc1ztjNu6mduFB41FO3kQDBH2uoa2PgdgWtaO-zLSrTDns3aGffp6VWYddxuPvA3T5VQDVjVLz6eJ2_d2N0B9sPmevKkIYEaI1-OClY_StB4TG3VdNUsfcaqymweg43scS4DZBa9DPGz7ZmC-HG_z1oVfaoZGfa5-XD-u0Oyuu91xYM-QOuccvsHco2GWUTsf52qH51mLvkpc5AkaO9tUzhN0rQLabOGSUBjJZyxIFrRsLLlDWDwPSr3k7BbB1fBUvsUiPShqI_CWKJjfqsEiHjrt3MbfcbLc12XqYuvrCHwtpi0JRP67TwnUKADP7SEP0FEdXz34prhXpJSvK49hmpUkKYCbsdntJFz4J6vB7BKLcb9B6MsWNH5N14yhn35UJZc7_IToLLnjG93KcOhSWNvXtP9vTXRVmxIJtYfnZOOI6wsTxZ3WQcAypxYA1PLzJzlVrZC2TkT9Jhcph_Y2uHCKm0CFka2nr8DHF2VyrEo1n4x_bjXve_BZigWgGgFE0Vn4NEUdLislf0jTXcVhTwVCCxtYGPkKE3NTpu-Djl3HjPUGRYFC3dXa10HWJiazfOITTUO85LEpVRb9j2lhQaX9dKe6toPipSpGwzsdkOaw1liuADV9J8ssMSkhs0W_CLKd2HaJioPKUVUJ0v2B5Z39DiLyJ_NDx6vZoGzO8bRUp2Atm_z3ccrsaKuO-brZBmG_gApoXLQXdBPwovyS36TXx0fcdqcFlH5yqADtK1Yn9Zjkp5gIzbPz-k8nUtBtu4_JOayzFJpWGheTrkoj-nBIvuOfQB1yJan9BVZPF-KzALb1JdTC28k0kK60Exf7r2lulwA_3_-Ydg8inZ1HHGUYouAb6Na1r4Pk70g4waTzLItIbDPOeATPfvV9xRJ7LT8Yq-pbfIg1rHKRjrCcQ8IeonjziGxfNnFiPy4ANLtg2F4Z_dwi0BPE9MFo0yO7gKELGGpGoOB4rcd8mCK_wnm6MyBbdqC5oftova8ymTqjqNEH24L9DwCk8F_jx8DJhPvgrOnMU0Gc691Ym5uQeF_aMwtn6AaauBVr2Qf8VBE89wz1-sxk2vI4nJQasW9Q8wanqw_pSxwMoc1XzG8MY2sNRRREt1Ex1nmR4HjVdDSYJmbnqYgREPiJ6zUTcJkNJDPQjo70R1TEVjV8v0a97Ca_Fijp8CG2ZcD4VIAM8kOF90RJlUeG74pCIscB6AGi7tmgcO9a5VdHfeeqomRlFKVN9Ipi255SZHpI8JiUaUGwpP71RE-JHo4baZ-g7Z6i_UJsh6-76LKwL3HWblb2shAazc5cqC-ySHohWfY6_zeFD02zeoVN5vr-SaoQjzywVfef9V4AS5cs8BqlIJYP6Lrzh2AC9GwtSpEw22NVT6rR4VAIbER1HuGbwnNmnCDwHaOoyzFwkVGU8uohlOG8QYPUnLLfl8LzKv2O6SLlwcdZA1Fxiftv_ziMA4qo9Q3NQ-bblDycx-oKMWzSAvbx4LrjYBOxVn-XErq7B7K_i2kkK1C9Ttf63zbemz0aPcdhXWiEnKArv-q3kj5y21FKlmI4TwZuTKV5xlu-mOfXXWmTZZysOT6my6xZXsNOa1N-LiigdU
Line truncated
|
||||
let cookie = "";
|
||||
|
||||
let mut creds = SoraCredentialSet::initialize_with_just_cookies_str(cookie);
|
||||
|
||||
let bearer = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MSJdLCJjbGllbnRfaWQiOiJhcHBfWDh6WTZ2VzJwUTl0UjNkRTduSzFqTDVnSCIsImV4cCI6MTc0NzQzOTIwMiwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7InVzZXJfaWQiOiJ1c2VyLTZOeEpmbEFIb0VCREp6Wmw5aVhocWJERyJ9LCJodHRwczovL2FwaS5vcGVuYWkuY29tL3Byb2ZpbGUiOnsiZW1haWwiOiJ2b2NvZGVzMjAyMEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0sImlhdCI6MTc0NjU3NTIwMiwiaXNzIjoiaHR0cHM6Ly9hdXRoLm9wZW5haS5jb20iLCJqdGkiOiJiYzM3NjgyYS1iZjJhLTQ1MmUtODFiMi1hNDRhMzY1ZGMzZTIiLCJuYmYiOjE3NDY1NzUyMDIsInB3ZF9hdXRoX3RpbWUiOjE3NDY1NzUyMDE2NjYsInNjcCI6WyJvcGVuaWQiLCJlbWFpbCIsInByb2ZpbGUiLCJvZmZsaW5lX2FjY2VzcyIsIm1vZGVsLnJlcXVlc3QiLCJtb2RlbC5yZWFkIiwib3JnYW5pemF0aW9uLnJlYWQiLCJvcmdhbml6YXRpb24ud3JpdGUiXSwic2Vzc2lvbl9pZCI6ImF1dGhzZXNzX2M0VXcyZWZUVE0xT0ZBZkFoTml0ck43dCIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTEzMTAxOTY3NjEyMzk2NzkzNzc3In0.JPi0OSP8T27JM_hYffctiWN2MaVVNzE-kQxbhgdHFhrirjQvNdIAfYbP5764-_3UhtM-waNNsLPx3rjTwHuDxC9319axCpCDvNJg55JZMEW-4r-juUecGFoF9VS3w57Znt3mXAk3LyB0adoGZq3Hs6Za6bqrTVyyil24iIj1kEapK0uiDKf4jW_YKR5R3ZV4igZoOAMglKSEIiQ2oR2yGLEj0Omi6XF8awe8RQ1IlAkvv-67fvUIFHU9lBHvRFgv6ktEUJSIz9FOCUADgGTUKhqXyvlT1xVeMcjifi0xpks39gmLcGhygVGsOHLf2j7DRCuSuYDd8_xqtsg_Qc58iMHwWuEIRaoPiTN_PMkT4gncmou7PnoUgRQMlpojGwMH8fyRSYsgFcg0wC0Q7QWlZvZCEMt_w8jhXk1Hn8k0wIKWH0L5v2PN6i1oDVjfXPXDq9X0Mp1vqgMwal_LOwsKB93SNHdtvzUD0ZCq9Nhr-eW-6xOF1YT9g5j3-Dk7cm97lJjmYlDEYBlDGCC34D5IBlZJy2H3Ojl68Ff3DYGsWsDiAidYAmoj2rUXio3got06l81L45ddXayrqMA9BizwmXjReJcnZZm7sCGSYTOBY5PeAo_I0i-TZe7sXoFppsO78KozLVrtfoCa6hUPlX0psJJbnbaI_IJualCuByR8gxg";
|
||||
|
||||
creds.jwt_bearer_token = Some(SoraJwtBearerToken::new(bearer.to_string())?);
|
||||
|
||||
//let _updated = maybe_upgrade_or_renew_session(&mut creds).await?;
|
||||
|
||||
let results = list_media(&creds).await.expect("should work");
|
||||
|
||||
println!("{:?}", results);
|
||||
|
||||
assert!(results.task_responses.len() > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod list_media;
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod bearer;
|
||||
pub mod image_gen;
|
||||
pub mod job;
|
||||
pub mod list_media;
|
||||
pub mod sentinel_refresh;
|
||||
pub mod upload;
|
||||
@@ -26,6 +26,14 @@ pub enum SoraError {
|
||||
/// }
|
||||
UnauthorizedCookieOrBearerExpired,
|
||||
|
||||
/// Failed to generate a bearer token despite the `200 OK` response.
|
||||
/// This has been generated by sending old/expired session cookies or empty cookies.
|
||||
FailedToGenerateBearer,
|
||||
|
||||
/// Something is wrong with the JWT bearer token.
|
||||
/// This error originates on our end as we try to parse the JWT.
|
||||
LocalJwtClaimsParseError(String),
|
||||
|
||||
/// Sora is having issues.
|
||||
/// Typically served from Cloudflare
|
||||
/// We preserve the message for debugging.
|
||||
@@ -55,6 +63,12 @@ impl Display for SoraError {
|
||||
Self::NoBearerTokenAvailable => {
|
||||
write!(f, "Unauthorized: no bearer token set")
|
||||
}
|
||||
Self::FailedToGenerateBearer => {
|
||||
write!(f, "Failed to generate bearer. Probably invalid session.")
|
||||
}
|
||||
Self::LocalJwtClaimsParseError(message) => {
|
||||
write!(f, "Local JWT claims parse error: {}", message)
|
||||
}
|
||||
Self::TooManyConcurrentTasks => {
|
||||
write!(f, "Too many concurrent tasks. Please wait.")
|
||||
}
|
||||
|
||||
@@ -8,7 +8,33 @@ pub async fn classify_general_http_error(response: reqwest::Response) -> SoraErr
|
||||
Ok(text) => text,
|
||||
Err(err) => return SoraError::ReqwestError(err),
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Old cookie -
|
||||
|
||||
Message: {
|
||||
"error": {
|
||||
"message": "Your authentication token has expired. Please try signing in again.",
|
||||
"type": "invalid_request_error",
|
||||
"param": null,
|
||||
"code": "token_expired"
|
||||
}
|
||||
}
|
||||
|
||||
No cookie -
|
||||
|
||||
Message: {
|
||||
"error": {
|
||||
"message": "Your authentication token has expired. Please try signing in again.",
|
||||
"type": "invalid_request_error",
|
||||
"param": null,
|
||||
"code": "token_expired"
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// TODO: I *think* this is just the bearer token, not the cookie.
|
||||
let cookie_expired =
|
||||
message.contains("Your authentication token has expired. Please try signing in again.")
|
||||
|| message.contains("token_expired");
|
||||
|
||||
@@ -4,7 +4,8 @@ use cookie::Cookie;
|
||||
use once_cell::sync::Lazy;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub const SESSION_COOKIE_NAME : &str = "__Secure-authjs.session-token";
|
||||
pub const SESSION_COOKIE_NAME_OLD : &str = "__Secure-authjs.session-token";
|
||||
pub const SESSION_COOKIE_NAME_NEW : &str = "__Secure-next-auth.session-token";
|
||||
|
||||
/// Cookies Sora.com mints that are not the session cookie. We can ignore these.
|
||||
/// This list is very likely to change over time.
|
||||
@@ -51,7 +52,9 @@ pub fn has_session_cookie(cookies_header: &str) -> AnyhowResult<SessionCookiePre
|
||||
for cookie in cookies {
|
||||
let cookie= cookie?;
|
||||
let cookie_name = cookie.name();
|
||||
if cookie_name == SESSION_COOKIE_NAME {
|
||||
if cookie_name == SESSION_COOKIE_NAME_NEW {
|
||||
return Ok(SessionCookiePresence::Present);
|
||||
} else if cookie_name == SESSION_COOKIE_NAME_NEW {
|
||||
return Ok(SessionCookiePresence::Present);
|
||||
} else if !IRRELEVANT_COOKIE_NAMES.contains(cookie_name) {
|
||||
unknown_cookie_count += 1;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! Adapted from 'jwt' crate
|
||||
|
||||
use crate::sora_error::SoraError;
|
||||
use anyhow::anyhow;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
|
||||
use base64::Engine;
|
||||
use chrono::{DateTime, Utc};
|
||||
use errors::AnyhowResult;
|
||||
use log::warn;
|
||||
use serde_json::Value;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
const SEPARATOR: &str = ".";
|
||||
|
||||
@@ -19,48 +20,53 @@ pub struct SoraJwtClaims {
|
||||
pub email_verified: bool,
|
||||
}
|
||||
|
||||
pub fn lightweight_sora_jwt_parse(token: &str) -> AnyhowResult<SoraJwtClaims> {
|
||||
pub fn lightweight_sora_jwt_parse(token: &str) -> Result<SoraJwtClaims, SoraError> {
|
||||
let [_header_str, claims_str, _signature_str] = split_components(token)?;
|
||||
|
||||
let claims = decode_base64(claims_str)?;
|
||||
let claims : serde_json::Map<String, Value> = serde_json::from_str(&claims)?;
|
||||
let claims = decode_base64(claims_str)
|
||||
.map_err(|err| SoraError::LocalJwtClaimsParseError(
|
||||
format!("failure to parse claims base64: {:?}", err)))?;
|
||||
|
||||
let claims : serde_json::Map<String, Value> = serde_json::from_str(&claims)
|
||||
.map_err(|err| SoraError::LocalJwtClaimsParseError(
|
||||
format!("failure to parse claims json: {:?}", err)))?;
|
||||
|
||||
let iat = claims.get("iat")
|
||||
.map(|val| val.as_number())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no iat claim"))?;
|
||||
.ok_or_else(|| jwt_error("no iat claim"))?;
|
||||
|
||||
let iat = iat.as_i64()
|
||||
.ok_or(anyhow!("iat is not a number"))?;
|
||||
.ok_or_else(|| jwt_error("iat is not a number"))?;
|
||||
|
||||
let iat = DateTime::from_timestamp(iat, 0)
|
||||
.ok_or(anyhow!("iat is not a valid timestamp"))?;
|
||||
.ok_or_else(|| jwt_error("iat is not a valid timestamp"))?;
|
||||
|
||||
let exp = claims.get("exp")
|
||||
.map(|val| val.as_number())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no exp claim"))?;
|
||||
.ok_or_else(|| jwt_error("no exp claim"))?;
|
||||
|
||||
let exp = exp.as_i64()
|
||||
.ok_or(anyhow!("exp is not a number"))?;
|
||||
.ok_or_else(|| jwt_error("exp is not a number"))?;
|
||||
|
||||
let exp = DateTime::from_timestamp(exp, 0)
|
||||
.ok_or(anyhow!("iat is not a valid timestamp"))?;
|
||||
.ok_or_else(|| jwt_error("iat is not a valid timestamp"))?;
|
||||
|
||||
let user_id = claims.get("https://api.openai.com/auth")
|
||||
.map(|val| val.get("user_id"))
|
||||
.flatten()
|
||||
.map(|val| val.as_str())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no user_id claim"))?;
|
||||
.ok_or_else(|| jwt_error("no user_id claim"))?;
|
||||
|
||||
let profile = claims.get("https://api.openai.com/profile")
|
||||
.ok_or(anyhow!("no user_id claim"))?;
|
||||
.ok_or_else(|| jwt_error("no user_id claim"))?;
|
||||
|
||||
let email = profile.get("email")
|
||||
.map(|val| val.as_str())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no email claim"))?;
|
||||
.ok_or_else(|| jwt_error("no email claim"))?;
|
||||
|
||||
let email_verified = profile.get("email_verified")
|
||||
.map(|val| val.as_bool())
|
||||
@@ -79,14 +85,19 @@ pub fn lightweight_sora_jwt_parse(token: &str) -> AnyhowResult<SoraJwtClaims> {
|
||||
})
|
||||
}
|
||||
|
||||
fn split_components(token: &str) -> AnyhowResult<[&str; 3]> {
|
||||
fn split_components(token: &str) -> Result<[&str; 3], SoraError> {
|
||||
let mut components = token.split(SEPARATOR);
|
||||
let header = components.next().ok_or(anyhow!("no header component"))?;
|
||||
let claims = components.next().ok_or(anyhow!("no claims component"))?;
|
||||
let signature = components.next().ok_or(anyhow!("no signature component"))?;
|
||||
let header = components.next()
|
||||
.ok_or_else(|| jwt_error("no header component"))?;
|
||||
|
||||
let claims = components.next()
|
||||
.ok_or_else(|| jwt_error("no claims component"))?;
|
||||
|
||||
let signature = components.next()
|
||||
.ok_or_else(|| jwt_error("no signature component"))?;
|
||||
|
||||
if components.next().is_some() {
|
||||
return Err(anyhow!("too many components"));
|
||||
return Err(jwt_error("too many components"));
|
||||
}
|
||||
|
||||
Ok([header, claims, signature])
|
||||
@@ -98,6 +109,10 @@ fn decode_base64(raw: &str) -> AnyhowResult<String> {
|
||||
Ok(json_str)
|
||||
}
|
||||
|
||||
fn jwt_error(reason: &str) -> SoraError {
|
||||
SoraError::LocalJwtClaimsParseError(reason.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::lightweight_sora_jwt_parse::lightweight_sora_jwt_parse;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
|
||||
import { IsDesktopApp } from "@storyteller/tauri-utils";
|
||||
import { CheckSoraSession, SoraSessionState } from "@storyteller/tauri-api";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
interface LoginModalProps {
|
||||
@@ -29,15 +30,11 @@ export function LoginModal({
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const totalSteps = 3;
|
||||
|
||||
const checkSession = async () => {
|
||||
// Return the current session state
|
||||
return hasSession;
|
||||
};
|
||||
|
||||
// Check session on component mount
|
||||
useEffect(() => {
|
||||
const initSession = async () => {
|
||||
const sessionExists = await checkSession();
|
||||
const result = await CheckSoraSession();
|
||||
const sessionExists = result.state === SoraSessionState.Valid;
|
||||
if (!sessionExists) {
|
||||
setIsOpen(true);
|
||||
setStep(1);
|
||||
@@ -57,10 +54,12 @@ export function LoginModal({
|
||||
try {
|
||||
if (IsDesktopApp()) {
|
||||
await invoke("open_sora_login_command");
|
||||
|
||||
// Add 3 second delay before setting session to true
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
setHasSession(true);
|
||||
const sessionExists = await checkSession();
|
||||
const result = await CheckSoraSession();
|
||||
const sessionExists = result.state === SoraSessionState.Valid;
|
||||
if (sessionExists) {
|
||||
setStep(step + 1);
|
||||
}
|
||||
@@ -168,6 +167,9 @@ export function LoginModal({
|
||||
alt="OpenAI Logo"
|
||||
className="w-72 h-72 mx-auto grow"
|
||||
/>
|
||||
{/*<div>
|
||||
<Button onClick={() => handleClose()}>Cancel (temporary)</Button>
|
||||
</div>*/}
|
||||
</div>
|
||||
);
|
||||
case 3:
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
|
||||
export * from './lib/settings/GetAppPreferences.js';
|
||||
export * from './lib/sora/CheckSoraSession.js';
|
||||
@@ -0,0 +1,18 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface CheckSoraSessionResult {
|
||||
state: SoraSessionState,
|
||||
maybe_account_email?: string,
|
||||
}
|
||||
|
||||
export enum SoraSessionState {
|
||||
NotSetUp = "not_set_up",
|
||||
ExpiredOrError = "expired_or_error",
|
||||
Valid = "valid",
|
||||
}
|
||||
|
||||
export const CheckSoraSession = async () : Promise<CheckSoraSessionResult> => {
|
||||
let result = await invoke("check_sora_session_command");
|
||||
console.log(">>> sora result", result);
|
||||
return (result as CheckSoraSessionResult);
|
||||
}
|
||||
Reference in new issue
Block a user