From 893a29f965eaf137f18bfcebfa5264e764a8461f Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 30 Oct 2025 19:51:43 -0400 Subject: [PATCH] post id --- .../grok_client/src/datatypes/api/mod.rs | 1 + .../grok_client/src/datatypes/api/post_id.rs | 6 ++ .../upload_image_and_generate_video.rs | 88 ++++++++++++++----- ...oct_30_rust_response_from_file_upload.json | 13 +++ .../grok_create_media_post.rs | 36 ++++---- .../src/requests/create_media_post/mod.rs | 3 +- .../requests/create_media_post/response.rs | 28 ++++++ .../grok_video_gen_chat_conversation.rs | 2 + 8 files changed, 140 insertions(+), 37 deletions(-) create mode 100644 crates/api_clients/grok_client/src/datatypes/api/post_id.rs create mode 100644 crates/api_clients/grok_client/src/requests/create_media_post/_metadata/oct_30_rust_response_from_file_upload.json create mode 100644 crates/api_clients/grok_client/src/requests/create_media_post/response.rs diff --git a/crates/api_clients/grok_client/src/datatypes/api/mod.rs b/crates/api_clients/grok_client/src/datatypes/api/mod.rs index f330c012f5..77d75655b2 100644 --- a/crates/api_clients/grok_client/src/datatypes/api/mod.rs +++ b/crates/api_clients/grok_client/src/datatypes/api/mod.rs @@ -1,5 +1,6 @@ pub mod baggage; pub mod file_id; +pub mod post_id; pub mod request_id; pub mod sentry_trace; pub mod svg_path_data; 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 new file mode 100644 index 0000000000..2bcd9b4f7c --- /dev/null +++ b/crates/api_clients/grok_client/src/datatypes/api/post_id.rs @@ -0,0 +1,6 @@ + +/// 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); 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 24706b6aca..66b3d346e0 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 @@ -1,5 +1,6 @@ use crate::credentials::grok_full_credentials::GrokFullCredentials; use crate::datatypes::api::file_id::FileId; +use crate::datatypes::api::post_id::PostId; use crate::datatypes::file_upload_spec::FileUploadSpec; use crate::error::grok_error::GrokError; use crate::error::grok_generic_api_error::GrokGenericApiError; @@ -19,17 +20,45 @@ pub struct UploadImageAndGenerateVideo<'a, P: AsRef> { // 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, + } -pub struct ImageUploadResult { +#[derive(Debug, Clone)] +pub struct ImageUploadAndGenerateVideoResult { + /// The ID of the "post" + /// This often is equivalent to the upload id / `image_file_id`, + /// and this is in the user-facing URL for website navigation to the video. + pub post_id: PostId, + + /// The ID of the file we uploaded + pub image_file_id: FileId, + + /// The ID of the video we're generating, which may or may not exist yet. pub video_file_id: Option, + + /// The URL for the video, which may or may not exist yet. pub video_url: Option, + + /// Whether the video generation has already completed. + /// Based on request `wait_for_generation`. + pub generation_is_complete: bool, } -pub async fn upload_image_and_generate_video>(args: UploadImageAndGenerateVideo<'_, P>) -> Result { +pub async fn upload_image_and_generate_video>(args: UploadImageAndGenerateVideo<'_, P>) + -> Result +{ info!("Uploading file to Grok..."); @@ -44,7 +73,7 @@ pub async fn upload_image_and_generate_video>(args: UploadImageAn info!("File URI: {:?}", upload_result.file_uri); info!("File Metadata ID: {:?}", upload_result.file_id); - let upload_file_id = match &upload_result.file_id { + let upload_file_id = match upload_result.file_id { Some(id) => id, None => { error!("Previous file upload failed. Cannot continue."); @@ -63,13 +92,13 @@ pub async fn upload_image_and_generate_video>(args: UploadImageAn let request = GrokCreateMediaPost { user_id: &args.full_credentials.client_secrets.user_id, - file_id: upload_file_id, + file_id: &upload_file_id, media_type: MediaPostType::UserUploadedImage, cookie: args.full_credentials.cookies.as_str(), request_timeout: args.individual_request_timeout, }; - let _post_result = request.send().await?; + let post_result = request.send().await?; info!("Posted Image."); @@ -89,9 +118,20 @@ pub async fn upload_image_and_generate_video>(args: UploadImageAn numbers: &args.full_credentials.client_secrets.numbers, }; - let video_gen_result = request.stream_only_video_id().await?; + let maybe_video_file_id; + let generation_is_complete; - let maybe_video_file_id = video_gen_result.video_file_id; + if args.wait_for_generation { + info!("Waiting for video generation..."); + let video_gen_result = request.send().await?; + maybe_video_file_id = video_gen_result.video_file_id; + generation_is_complete = true; + } else { + info!("Streaming response partially..."); + let video_gen_result = request.stream_only_video_id().await?; + maybe_video_file_id = video_gen_result.video_file_id; + generation_is_complete = false; + } let maybe_video_url = maybe_video_file_id .as_ref() @@ -118,9 +158,12 @@ pub async fn upload_image_and_generate_video>(args: UploadImageAn info!("Media Liked"); - Ok(ImageUploadResult { + Ok(ImageUploadAndGenerateVideoResult { + post_id: post_result.post_id, + image_file_id: upload_file_id, video_file_id: maybe_video_file_id, video_url: maybe_video_url, + generation_is_complete, }) } @@ -145,23 +188,23 @@ mod tests { async fn test_upload_image_and_generate_video() -> AnyhowResult<()> { setup_test_logging(LevelFilter::Info); + //let image_path = "/Users/bt/Pictures/Zelda 64 Art/FCgYX6tWEAEhpsy.jpg"; + //let prompt = "our hero link plunges the sword into the pedestal, the temple is glowing with a blue aura"; + + let image_path = "/Users/bt/Pictures/Midjourney/hipster_girl.png"; + let maybe_prompt = Some("an anime girl runs away from a giant t-rex"); + let cookies = get_typed_test_cookies()?; let secrets = request_client_secrets(RequestClientSecretsArgs { cookies: &cookies, }).await?; - //let image_path = "/Users/bt/Pictures/Zelda 64 Art/FCgYX6tWEAEhpsy.jpg"; - //let prompt = "our hero link plunges the sword into the pedestal, the temple is glowing with a blue aura"; - - let image_path = "/Users/bt/Pictures/Wallpaper/digital_topography_midjourney.png"; - let maybe_prompt = Some("the mountains are digitial and lights flicker up and down the topography. the camera pans gently by"); - - println!("Verification Token: {:?}", secrets.verification_token); - println!("Sentry Trace: {:?}", secrets.sentry_trace); - println!("Numbers: {:?}", secrets.numbers); - println!("Svg Path: {:?}", secrets.svg_path_data); - println!("Baggage: {:?}", secrets.baggage); + println!("[test] Verification Token: {:?}", secrets.verification_token); + println!("[test] Sentry Trace: {:?}", secrets.sentry_trace); + println!("[test] Numbers: {:?}", secrets.numbers); + println!("[test] Svg Path: {:?}", secrets.svg_path_data); + println!("[test] Baggage: {:?}", secrets.baggage); let credentials = GrokFullCredentials::from_cookies_and_client_secrets(cookies, secrets); @@ -170,10 +213,13 @@ mod tests { file: FileUploadSpec::Path(image_path), prompt: maybe_prompt, individual_request_timeout: None, + wait_for_generation: false, }).await?; - println!("Video File ID: {:?}", video_result.video_file_id); - println!("Video URL: {:?}", video_result.video_url); + println!("[test] Post ID: {:?}", video_result.post_id); + println!("[test] Image File ID: {:?}", video_result.image_file_id); + println!("[test] Video File ID: {:?}", video_result.video_file_id); + println!("[test] Video URL: {:?}", video_result.video_url); assert_eq!(1, 2); diff --git a/crates/api_clients/grok_client/src/requests/create_media_post/_metadata/oct_30_rust_response_from_file_upload.json b/crates/api_clients/grok_client/src/requests/create_media_post/_metadata/oct_30_rust_response_from_file_upload.json new file mode 100644 index 0000000000..b571258ca2 --- /dev/null +++ b/crates/api_clients/grok_client/src/requests/create_media_post/_metadata/oct_30_rust_response_from_file_upload.json @@ -0,0 +1,13 @@ +{ + "post": { + "id": "3c896963-8e6b-4e71-83e1-4253b2bf2ac8", + "userId":"85980643-ffab-4984-a3de-59a608c47d7f", + "createTime":"2025-10-30T23:22:19.528702688Z", + "prompt":"", + "mediaType":"MEDIA_POST_TYPE_IMAGE", + "mediaUrl":"https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/3c896963-8e6b-4e71-83e1-4253b2bf2ac8/content", + "mimeType":"image/jpeg", + "audioUrls":[], + "childPosts":[] + } +} \ No newline at end of file diff --git a/crates/api_clients/grok_client/src/requests/create_media_post/grok_create_media_post.rs b/crates/api_clients/grok_client/src/requests/create_media_post/grok_create_media_post.rs index 87d16f3f5f..8e725820ca 100644 --- a/crates/api_clients/grok_client/src/requests/create_media_post/grok_create_media_post.rs +++ b/crates/api_clients/grok_client/src/requests/create_media_post/grok_create_media_post.rs @@ -1,10 +1,12 @@ use crate::client::browser_user_agents::FIREFOX_143_MAC_USER_AGENT; use crate::datatypes::api::file_id::FileId; +use crate::datatypes::api::post_id::PostId; use crate::datatypes::api::user_id::UserId; use crate::error::grok_client_error::GrokClientError; use crate::error::grok_error::GrokError; use crate::error::grok_generic_api_error::GrokGenericApiError; use crate::requests::create_media_post::request::CreateMediaPostWireRequest; +use crate::requests::create_media_post::response::CreateMediaPostRawResponse; use crate::requests::upload_file::grok_upload_file::{GrokUploadFile, GrokUploadFileResponse}; use crate::utils::user_and_file_id_to_image_url::user_and_file_id_to_image_url; use log::{error, info}; @@ -32,8 +34,9 @@ pub enum MediaPostType { } /// Response type -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct GrokCreateMediaPostResponse { + pub post_id: PostId, } impl <'a> GrokCreateMediaPost<'a> { @@ -47,9 +50,17 @@ impl <'a> GrokCreateMediaPost<'a> { // TODO: Headers were from Chromium, not Firefox. Partial implementation. let mut request_builder = client.post(CREATE_MEDIA_POST_URL) + // TODO: Missing header "baggage" + // TODO: Missing header "sentry-trace" + // TODO: Missing header "traceparent" + // TODO: Missing header "traceparent" + // TODO: Missing header "x-statsig-id" + // TODO: Missing header "x-xai-request-id" + //.header("sec-ch-ua", "") // NB: Not present in firefox + //.header("sec-ch-ua-mobile", "") // NB: Not present in firefox + //.header("sec-ch-ua-platform", "") // NB: Not present in firefox .header(ACCEPT, "*/*") .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") - // TODO: Missing header "baggage" .header(CACHE_CONTROL, "no-cache") .header(CONTENT_TYPE, "application/json") .header(COOKIE, self.cookie.to_string()) @@ -57,17 +68,9 @@ impl <'a> GrokCreateMediaPost<'a> { .header(PRAGMA, "no-cache") .header("priority", "u=1, i") .header(REFERER, "https://grok.com/imagine/favorites") - //.header("sec-ch-ua", "") // TODO - //.header("sec-ch-ua-mobile", "") // TODO - //.header("sec-ch-ua-platform", "") // TODO .header("sec-fetch-dest", "empty") .header("sec-fetch-mode", "cors") .header("sec-fetch-site", "same-origin") - // TODO: Missing header "sentry-trace" - // TODO: Missing header "traceparent" - // TODO: Missing header "traceparent" - // TODO: Missing header "x-statsig-id" - // TODO: Missing header "x-xai-request-id" .header(USER_AGENT, FIREFOX_143_MAC_USER_AGENT); //info!("Sending..."); @@ -106,23 +109,24 @@ impl <'a> GrokCreateMediaPost<'a> { let status = response.status(); - let response_body = &response.text() + let response_body = response.text() .await .map_err(|err| { error!("Error reading Grok create media response body: {:?}", err); GrokGenericApiError::WreqError(err) })?; - // TODO: + // TODO: Handle errors (Cloudflare, Grok, etc.) if !status.is_success() { error!("Upload file request returned an error (code {}) : {:?}", status.as_u16(), response_body); //return Err(classify_general_http_status_code_and_body(status, response_body)); } - //let response : GrokApiUploadFileResponse = serde_json::from_str(response_body) - // .map_err(|err| GrokGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; + let response : CreateMediaPostRawResponse = serde_json::from_str(&response_body) + .map_err(|err| GrokGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - Ok(GrokCreateMediaPostResponse{ + Ok(GrokCreateMediaPostResponse { + post_id: PostId(response.post.id), }) } } @@ -153,6 +157,8 @@ mod tests { let result = request.send().await?; println!("Result: {:?}", result); + println!("Post ID: {:?}", result.post_id); + assert_eq!(1, 2); Ok(()) } diff --git a/crates/api_clients/grok_client/src/requests/create_media_post/mod.rs b/crates/api_clients/grok_client/src/requests/create_media_post/mod.rs index a8e788f5b4..9fc4c9c3f9 100644 --- a/crates/api_clients/grok_client/src/requests/create_media_post/mod.rs +++ b/crates/api_clients/grok_client/src/requests/create_media_post/mod.rs @@ -1,2 +1,3 @@ pub mod grok_create_media_post; -pub (super) mod request; \ No newline at end of file +pub (super) mod request; +mod response; \ No newline at end of file diff --git a/crates/api_clients/grok_client/src/requests/create_media_post/response.rs b/crates/api_clients/grok_client/src/requests/create_media_post/response.rs new file mode 100644 index 0000000000..feb01ffc9a --- /dev/null +++ b/crates/api_clients/grok_client/src/requests/create_media_post/response.rs @@ -0,0 +1,28 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct CreateMediaPostRawResponse { + pub post: PostData, +} + +#[derive(Deserialize)] +pub struct PostData { + /// The identifier for the post + pub id: String, +} + +/* +{ + "post": { + "id": "3c896963-8e6b-4e71-83e1-4253b2bf2ac8", + "userId":"85980643-ffab-4984-a3de-59a608c47d7f", + "createTime":"2025-10-30T23:22:19.528702688Z", + "prompt":"", + "mediaType":"MEDIA_POST_TYPE_IMAGE", + "mediaUrl":"https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/3c896963-8e6b-4e71-83e1-4253b2bf2ac8/content", + "mimeType":"image/jpeg", + "audioUrls":[], + "childPosts":[] + } +} +*/ diff --git a/crates/api_clients/grok_client/src/requests/video_chat/grok_video_gen_chat_conversation.rs b/crates/api_clients/grok_client/src/requests/video_chat/grok_video_gen_chat_conversation.rs index 0e29bf1010..ed294841e8 100644 --- a/crates/api_clients/grok_client/src/requests/video_chat/grok_video_gen_chat_conversation.rs +++ b/crates/api_clients/grok_client/src/requests/video_chat/grok_video_gen_chat_conversation.rs @@ -110,6 +110,7 @@ impl <'a> GrokVideoGenChatConversationBuilder<'a> { }) } + pub async fn send(&self) -> Result { info!("Configuring client and request..."); let client = Self::build_client()?; @@ -164,6 +165,7 @@ impl <'a> GrokVideoGenChatConversationBuilder<'a> { }) } + fn build_client() -> Result { Ok(Client::builder() .emulation(Emulation::Firefox143)