diff --git a/crates/api_clients/grok_client/src/datatypes/api/post_id.rs b/crates/api_clients/grok_client/src/datatypes/api/post_id.rs index 2bcd9b4f7c..3315dc9bb3 100644 --- a/crates/api_clients/grok_client/src/datatypes/api/post_id.rs +++ b/crates/api_clients/grok_client/src/datatypes/api/post_id.rs @@ -1,6 +1,12 @@ -/// Type for Post IDs. -/// Post IDs are UUIds. +/// Type for Post IDs. +/// Post IDs are UUIds. /// These are often shared with media/image uploads' `FileId`s, but seldom video generation `FileId`s #[derive(Clone, Debug)] pub struct PostId(pub String); + +impl PostId { + pub fn to_string(&self) -> String { + self.0.clone() + } +} diff --git a/crates/api_clients/grok_client/src/recipes/mod.rs b/crates/api_clients/grok_client/src/recipes/mod.rs index 1d09adac78..e774ecd05b 100644 --- a/crates/api_clients/grok_client/src/recipes/mod.rs +++ b/crates/api_clients/grok_client/src/recipes/mod.rs @@ -1,2 +1,3 @@ pub mod request_client_secrets; -mod upload_image_and_generate_video; \ No newline at end of file +pub mod upload_image_and_generate_video; +pub mod upload_image_and_generate_video_with_retry; \ No newline at end of file diff --git a/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video.rs b/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video.rs index d3b8582aaf..5da136d57c 100644 --- a/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video.rs +++ b/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video.rs @@ -32,7 +32,6 @@ pub struct UploadImageAndGenerateVideo<'a, P: AsRef> { pub wait_for_generation: bool, pub individual_request_timeout: Option, - } #[derive(Debug, Clone)] diff --git a/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video_with_retry.rs b/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video_with_retry.rs new file mode 100644 index 0000000000..b7f336515b --- /dev/null +++ b/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video_with_retry.rs @@ -0,0 +1,24 @@ +use std::path::Path; +use std::time::Duration; +use crate::credentials::grok_full_credentials::GrokFullCredentials; +use crate::datatypes::file_upload_spec::FileUploadSpec; + +pub struct UploadImageAndGenerateVideoWithRetry<'a, P: AsRef> { + pub credentials: &'a GrokFullCredentials, + + // NB: Must be owned. + pub file: FileUploadSpec

, + + /// Video generation prompt + pub prompt: Option<&'a str>, + + /// Wait for the full video to be generated before returning + /// By default, the endpoint stays open for 30-ish seconds while + /// the video generates and streams back JSON progress updates. + /// If we set this to false, we'll wait for the video ID and exit + /// the request early, before the video is finished asynchronously + /// generating. + pub wait_for_generation: bool, + + pub individual_request_timeout: Option, +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs b/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs index 000f236273..18d8f34738 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs @@ -1,6 +1,7 @@ use crate::core::artcraft_error::ArtcraftError; use base64::DecodeError; use errors::AnyhowError; +use grok_client::error::grok_error::GrokError; use midjourney_client::error::midjourney_error::MidjourneyError; use openai_sora_client::error::sora_error::SoraError; use storyteller_client::error::storyteller_error::StorytellerError; @@ -53,6 +54,7 @@ pub enum BadInputReason { #[derive(Debug)] pub enum MissingCredentialsReason { + NeedsGrokCredentials, NeedsFalApiKey, NeedsMidjourneyCredentials, NeedsMidjourneyUserId, @@ -76,6 +78,7 @@ pub enum BillingProvider { #[derive(Debug)] pub enum ProviderFailureReason { + GrokError(GrokError), //Fal(FalErrorPlus), MidjourneyError(MidjourneyError), /// NB: The midjourney client doesn't categorize all errors, so we have to do so on our end. @@ -113,6 +116,10 @@ impl GenerateError { pub fn needs_fal_api_key() -> Self { Self::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) } + + pub fn needs_grok_credentials() -> Self { + Self::MissingCredentials(MissingCredentialsReason::NeedsGrokCredentials) + } pub fn needs_midjourney_credentials() -> Self { Self::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyCredentials) @@ -150,6 +157,12 @@ impl From for GenerateError { // } //} +impl From for GenerateError { + fn from(value: GrokError) -> Self { + Self::ProviderFailure(ProviderFailureReason::GrokError(value)) + } +} + impl From for GenerateError { fn from(value: MidjourneyError) -> Self { Self::ProviderFailure(ProviderFailureReason::MidjourneyError(value)) diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs index e4a2b19eac..c3565a7f5b 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs @@ -1,6 +1,7 @@ use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason}; use crate::core::commands::enqueue::image_to_video::generic::handle_video::handle_video; +use crate::core::commands::enqueue::image_to_video::grok::handle_grok_video::handle_grok_video; use crate::core::commands::enqueue::image_to_video::sora2::handle_sora2_video::handle_sora2_video; use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus}; @@ -20,12 +21,16 @@ use log::{error, info, warn}; use serde_derive::{Deserialize, Serialize}; use tauri::{AppHandle, State}; use tokens::tokens::media_files::MediaFileToken; +use crate::services::grok::state::grok_credential_manager::GrokCredentialManager; /// This is used in the Tauri command bridge. /// Don't change the serializations without coordinating with the frontend. #[derive(Deserialize, Debug, Copy, Clone)] #[serde(rename_all = "snake_case")] pub enum VideoModel { + #[serde(rename = "grok")] + Grok, + #[serde(rename = "kling_1.6_pro")] Kling16Pro, @@ -115,6 +120,7 @@ pub async fn enqueue_image_to_video_command( app_data_root: State<'_, AppDataRoot>, provider_priority_store: State<'_, ProviderPriorityStore>, task_database: State<'_, TaskDatabase>, + grok_creds_manager: State<'_, GrokCredentialManager>, storyteller_creds_manager: State<'_, StorytellerCredentialManager>, sora_task_queue: State<'_, SoraTaskQueue>, sora_creds_manager: State<'_, SoraCredentialManager>, @@ -129,6 +135,7 @@ pub async fn enqueue_image_to_video_command( &app_data_root, &provider_priority_store, &task_database, + &grok_creds_manager, &sora_creds_manager, &storyteller_creds_manager, ).await; @@ -200,11 +207,21 @@ pub async fn handle_request( app_data_root: &AppDataRoot, provider_priority_store: &ProviderPriorityStore, task_database: &TaskDatabase, + grok_creds_manager: &GrokCredentialManager, sora_creds_manager: &SoraCredentialManager, storyteller_creds_manager: &StorytellerCredentialManager, ) -> Result { let result = match request.model { + Some(VideoModel::Grok) => { + handle_grok_video( + &request, + app, + app_data_root, + app_env_configs, + grok_creds_manager, + ).await + } Some(VideoModel::Sora2) => { handle_sora2_video( &request, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video_artcraft.rs index d9112d57af..b8c6a9aaa4 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video_artcraft.rs @@ -71,6 +71,11 @@ pub async fn handle_video_artcraft( None => { return Err(GenerateError::no_model_specified()); } + Some(VideoModel::Grok) => { + return Err(GenerateError::AnyhowError( + anyhow!("wrong logic: another branch should handle this: {:?}", + request.model))); + } Some(VideoModel::Sora2) => { return Err(GenerateError::AnyhowError( anyhow!("wrong logic: another branch should handle this: {:?}", diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/grok/handle_grok_video.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/grok/handle_grok_video.rs new file mode 100644 index 0000000000..f91a7f9b5f --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/grok/handle_grok_video.rs @@ -0,0 +1,125 @@ +use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError}; +use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand; +use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand; +use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation}; +use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; +use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent; +use crate::core::events::generation_events::common::GenerationModel; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::provider_priority::{Provider, ProviderPriorityStore}; +use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir; +use crate::services::grok::state::grok_credential_manager::GrokCredentialManager; +use crate::services::grok::util::upload_image_media_file_to_grok::{upload_image_media_file_to_grok, UploadImageMediaFileToGrok}; +use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; +use crate::services::sora::state::sora_task_queue::SoraTaskQueue; +use crate::services::sora::utils::upload_images_to_sora::{upload_images_to_sora, UploadImagesToSoraArgs}; +use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use enums::common::generation_provider::GenerationProvider; +use enums::tauri::tasks::task_type::TaskType; +use grok_client::credentials::grok_cookies::GrokCookies; +use grok_client::credentials::grok_full_credentials::GrokFullCredentials; +use grok_client::datatypes::file_upload_spec::FileUploadSpec; +use grok_client::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs}; +use grok_client::recipes::upload_image_and_generate_video::{upload_image_and_generate_video, UploadImageAndGenerateVideo}; +use log::{error, info, warn}; +use openai_sora_client::recipes::generate_sora2_video::generate_sora2_video_with_session_auto_renew::generate_sora2_video_with_session_auto_renew; +use openai_sora_client::requests::generate_sora2_video::generate_sora2_video::{GenerateSora2VideoArgs, Orientation}; +use std::time::Duration; +use tauri::AppHandle; + +const GROK_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds + +pub async fn handle_grok_video( + request: &EnqueueImageToVideoRequest, + app: &AppHandle, + app_data_root: &AppDataRoot, + app_env_configs: &AppEnvConfigs, + grok_credential_manager: &GrokCredentialManager, +) -> Result { + + let creds = match grok_credential_manager.maybe_copy_full_credentials()? { + Some(creds) => creds, + None => { + ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Grok, &app); + return Err(GenerateError::needs_grok_credentials()); + }, + }; + + let image_token = match request.image_media_token.as_ref() { + Some(token) => token, + None => { + return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages { min: 1, max: 1, provided: 0 })) + } + }; + + info!("Downloading image media file..."); + + let local_image_file = download_media_file_to_temp_dir( + &app_env_configs, + &app_data_root, + image_token, + ).await?; + + info!("Calling Grok Video generate..."); + + let upload = upload_image_and_generate_video(UploadImageAndGenerateVideo { + full_credentials: &creds, + file: FileUploadSpec::Path(local_image_file.path()), + prompt: request.prompt.as_deref(), + wait_for_generation: false, + individual_request_timeout: Some(GROK_IMAGE_UPLOAD_TIMEOUT) + }).await; + + let post_id = match upload { + Err(err) => { + error!("Failed to use Grok video: {:?}", err); + return Err(GenerateError::from(err)); + } + Ok(result) => { + info!("Successfully enqueued Grok Video: post_id = {:?}", result.post_id); + result.post_id + } + }; + + Ok(TaskEnqueueSuccess { + provider: GenerationProvider::Sora, + model: Some(GenerationModel::Sora2), + provider_job_id: Some(post_id.to_string()), + task_type: TaskType::VideoGeneration, + }) +} + +async fn get_grok_creds(app: &AppHandle, grok_credential_manager: &GrokCredentialManager) -> Result { + if let Some(creds) = grok_credential_manager.maybe_copy_full_credentials()? { + return Ok(creds); + } + + let cookies = match grok_credential_manager.maybe_copy_cookie_store()? { + Some(cookies) => cookies.to_cookie_string(), + None => { + ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Grok, &app); + return Err(GenerateError::needs_grok_credentials()) + } + }; + + let cookies = GrokCookies::new(cookies); + + let upgraded = request_client_secrets(RequestClientSecretsArgs { + cookies: &cookies, + }).await; + + match upgraded { + Err(err) => { + error!("Failed to fetch Grok client secrets: {}", err); // NB: Fall-through + } + Ok(secrets) => { + let full_creds = GrokFullCredentials::from_cookies_and_client_secrets(cookies, secrets); + grok_credential_manager.replace_full_credentials(full_creds.clone())?; + return Ok(full_creds) + } + } + + ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Grok, &app); + Err(GenerateError::needs_grok_credentials()) +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/grok/mod.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/grok/mod.rs new file mode 100644 index 0000000000..045dcc9439 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/grok/mod.rs @@ -0,0 +1 @@ +pub (super) mod handle_grok_video; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/mod.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/mod.rs index 48074cb5ee..895bd733e0 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/mod.rs @@ -1,3 +1,4 @@ pub mod enqueue_image_to_video_command; mod generic; -pub mod sora2; \ No newline at end of file +pub mod sora2; +mod grok; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/grok/mod.rs b/crates/desktop/artcraft/src/services/grok/mod.rs index 132c03a929..b94e311046 100644 --- a/crates/desktop/artcraft/src/services/grok/mod.rs +++ b/crates/desktop/artcraft/src/services/grok/mod.rs @@ -1,3 +1,4 @@ pub mod commands; pub mod state; +pub mod util; pub mod windows; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/grok/state/grok_credential_holder.rs b/crates/desktop/artcraft/src/services/grok/state/grok_credential_holder.rs index 110d6dc798..119d36f764 100644 --- a/crates/desktop/artcraft/src/services/grok/state/grok_credential_holder.rs +++ b/crates/desktop/artcraft/src/services/grok/state/grok_credential_holder.rs @@ -1,5 +1,6 @@ use cookie_store::cookie_store::CookieStore; use grok_client::credentials::grok_client_secrets::GrokClientSecrets; +use grok_client::credentials::grok_full_credentials::GrokFullCredentials; use grok_client::credentials::grok_user_data::GrokUserData; #[derive(Clone)] @@ -9,6 +10,10 @@ pub struct GrokCredentialHolder { /// The Grok client consumes a string-only form (rather than this cookie jar) pub browser_cookies: Option, + /// Full credentials. + /// NOT PERSISTED TO DISK. + pub grok_full_credentials: Option, + /// Email, user id, etc. /// These can be persisted. pub grok_user_data: Option, @@ -23,6 +28,7 @@ impl GrokCredentialHolder { pub fn empty() -> Self { Self { browser_cookies: None, + grok_full_credentials: None, grok_user_data: None, grok_client_secrets: None, } diff --git a/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs b/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs index 5388f6add7..fad90684f6 100644 --- a/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs +++ b/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs @@ -4,6 +4,8 @@ use crate::services::grok::state::grok_serializable_state::{GrokSerializableStat use cookie_store::cookie_store::CookieStore; use errors::AnyhowResult; use grok_client::credentials::grok_client_secrets::GrokClientSecrets; +use grok_client::credentials::grok_cookies::GrokCookies; +use grok_client::credentials::grok_full_credentials::GrokFullCredentials; use grok_client::credentials::grok_user_data::GrokUserData; use log::{info, warn}; use std::fs::read_to_string; @@ -50,6 +52,7 @@ impl GrokCredentialManager { credential_data = Arc::new(RwLock::new(GrokCredentialHolder { browser_cookies: maybe_cookies, grok_user_data: user_data, + grok_full_credentials: None, // NB: We don't want to keep this on disk. It goes stale. grok_client_secrets: None, // NB: We don't want to keep this on disk. It goes stale. })); } @@ -68,6 +71,29 @@ impl GrokCredentialManager { } } + pub fn maybe_copy_cookie_header_string(&self) -> anyhow::Result> { + let maybe_cookies = self.maybe_copy_cookie_store()?; + let maybe_cookies = maybe_cookies.map(|cookies| { + cookies.to_cookie_string() + }); + Ok(maybe_cookies) + } + + pub fn maybe_copy_typed_cookies(&self) -> anyhow::Result> { + let maybe_cookies = self.maybe_copy_cookie_header_string()?; + let maybe_cookies = maybe_cookies.map(|cookies| { + GrokCookies::new(cookies) + }); + Ok(maybe_cookies) + } + + pub fn maybe_copy_full_credentials(&self) -> anyhow::Result> { + match self.credential_data.read() { + Err(err) => Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), + Ok(holder) => Ok(holder.grok_full_credentials.clone()), + } + } + pub fn maybe_copy_client_secrets(&self) -> anyhow::Result> { match self.credential_data.read() { Err(err) => Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), @@ -85,6 +111,16 @@ impl GrokCredentialManager { } } + pub fn replace_full_credentials(&self, creds: GrokFullCredentials) -> anyhow::Result<()> { + match self.credential_data.write() { + Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), + Ok(mut holder) => { + holder.grok_full_credentials = Some(creds); + Ok(()) + } + } + } + pub fn replace_client_secrets(&self, secrets: GrokClientSecrets) -> anyhow::Result<()> { match self.credential_data.write() { Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), diff --git a/crates/desktop/artcraft/src/services/grok/util/mod.rs b/crates/desktop/artcraft/src/services/grok/util/mod.rs new file mode 100644 index 0000000000..e9dc4512f5 --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/util/mod.rs @@ -0,0 +1 @@ +pub mod upload_image_media_file_to_grok; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/grok/util/upload_image_media_file_to_grok.rs b/crates/desktop/artcraft/src/services/grok/util/upload_image_media_file_to_grok.rs new file mode 100644 index 0000000000..fb120177d8 --- /dev/null +++ b/crates/desktop/artcraft/src/services/grok/util/upload_image_media_file_to_grok.rs @@ -0,0 +1,80 @@ +use crate::core::state::app_env_configs::app_env_configs_serializeable::StorytellerApiHost; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; +use crate::core::utils::get_url_file_extension::get_url_file_extension; +use crate::core::utils::simple_http_download::simple_http_download; +use crate::services::grok::state::grok_credential_manager::GrokCredentialManager; +use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; +use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use anyhow::anyhow; +use errors::AnyhowResult; +use grok_client::datatypes::api::file_id::FileId; +use grok_client::datatypes::file_upload_spec::FileUploadSpec; +use grok_client::requests::upload_file::grok_upload_file::GrokUploadFile; +use log::{error, info}; +use openai_sora_client::creds::sora_credential_set::SoraCredentialSet; +use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest}; +use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; +use std::time::Duration; +use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet; +use storyteller_client::endpoints::media_files::get_media_file::get_media_file; +use storyteller_client::utils::api_host::ApiHost; +use tokens::tokens::media_files::MediaFileToken; + +const GROK_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds + +pub struct UploadImageMediaFileToGrok<'a> { + pub storyteller_host: &'a ApiHost, + pub image_media_token: &'a MediaFileToken, + + pub app_data_root: &'a AppDataRoot, + pub grok_creds_manager: &'a GrokCredentialManager, +} + +pub struct UploadImageMediaFileToGrokResult { + pub grok_file_id: FileId, +} + +pub async fn upload_image_media_file_to_grok( + args: UploadImageMediaFileToGrok<'_>, +) -> AnyhowResult { + + info!("Calling get media file API: {:?}", args.storyteller_host); + + info!("Using media token: {:?}", args.image_media_token); + + let response = get_media_file( + args.storyteller_host, + args.image_media_token + ).await?; + + let media_file_url = &response.media_file.media_links.cdn_url; + let extension_with_dot = get_url_file_extension(media_file_url) + .map(|ext| format!(".{}", ext)) + .unwrap_or_else(|| ".png".to_string()); + + let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot); + let filename = args.app_data_root.downloads_dir().path().join(&filename); + + simple_http_download(&media_file_url, &filename).await?; + + let cookies = args.grok_creds_manager.maybe_copy_cookie_header_string()? + .ok_or_else(|| anyhow!("Missing Grok cookies"))?; + + info!("Uploading image to Grok..."); + + let upload = GrokUploadFile { + file: FileUploadSpec::Path(filename), + cookie: cookies, + request_timeout: Some(GROK_IMAGE_UPLOAD_TIMEOUT), + }; + + let response = upload.upload().await?; + + let file_id = response.file_id + .ok_or_else(|| anyhow!("Media upload did not produce a file_id!"))?; + + Ok(UploadImageMediaFileToGrokResult { + grok_file_id: file_id, + }) +}