upload file wip

This commit is contained in:
Brandon Thomas committed 2025-10-22 00:23:34 -04:00
1 parent 515f2b9c28
commit a85a5693f9
10 files changed
+342

No files matched your search

Generated
+1
View File
@@ -4673,6 +4673,7 @@ name = "grok_client"
version = "0.0.1"
dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cloudflare_errors",
"cloudflare_mitigation",
@@ -23,6 +23,7 @@ errors.workspace = true
# External
anyhow.workspace = true
base64.workspace = true
chrono.workspace = true
futures.workspace = true
log.workspace = true
@@ -19,6 +19,15 @@ pub enum GrokClientError {
/// Error sending to an open websocket.
WebsocketSendError(wreq::Error),
/// Can't open a local file for uploading.
CannotOpenLocalFileForUpload(std::io::Error),
/// Can't read a local file for uploading.
CannotReadLocalFileForUpload(std::io::Error),
/// File for upload has an invalid path.
FileForUploadHasInvalidPath,
// /// An error reading the file for upload.
// FileForUploadReadError(std::io::Error),
//
@@ -66,6 +75,9 @@ impl Display for GrokClientError {
Self::WebsocketLockError => write!(f, "Websocket lock error"),
Self::WebsocketReadError(err) => write!(f, "Websocket read error: {}", err),
Self::WebsocketSendError(err) => write!(f, "Websocket send error: {}", err),
Self::CannotOpenLocalFileForUpload(err) => write!(f, "Cannot open local file for upload: {}", err),
Self::CannotReadLocalFileForUpload(err) => write!(f, "Cannot read local file for upload: {}", err),
Self::FileForUploadHasInvalidPath => write!(f, "File for upload has invalid path"),
//Self::FileForUploadReadError(err) => write!(f, "Error reading file for upload: {}", err),
//Self::FileForUploadHasInvalidPath => write!(f, "The file path provided for upload is invalid."),
@@ -0,0 +1,83 @@
Upload Image -> Video Flow
==========================
1. Upload Image
Endpoint: https://grok.com/rest/app-chat/upload-file
Response Fields:
{
**"fileMetadataId": "21a79085-e206-4b0b-88ac-5f2b7a453e45",**
**"fileUri": "users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content",**
"fileMimeType": "image/jpeg",
"fileName": "0_0.jpeg",
"parsedFileUri": "",
"createTime": "2025-10-21T23:40:41.784448Z",
"fileSource": "SELF_UPLOAD_FILE_SOURCE"
}
File becomes a 480x640 copy:
https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content
2. "Create Media Post" **(needed?)**
Endpoint: https://grok.com/rest/media/post/create
Request Fields:
{
"mediaType": "MEDIA_POST_TYPE_IMAGE",
"mediaUrl": "https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content"
}
Response:
{
"post": {
"id": "21a79085-e206-4b0b-88ac-5f2b7a453e45",
"userId": "85980643-ffab-4984-a3de-59a608c47d7f",
"createTime": "2025-10-21T23:40:41.993980477Z",
"prompt": "",
"mediaType": "MEDIA_POST_TYPE_IMAGE",
"mediaUrl": "https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content",
"mimeType": "image/jpeg",
"audioUrls": [],
"childPosts": []
}
}
3. Chat Conversation for Video
Endpoint: https://grok.com/rest/app-chat/conversations/new
Request:
{
"temporary":true,
"modelName":"grok-3",
"message":"https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content --mode=normal",
"fileAttachments":["21a79085-e206-4b0b-88ac-5f2b7a453e45"],
"toolOverrides": {
"videoGen":true
}
}
Generate Image -> Video Flow
============================
(TODO)
3. Chat Conversation for Video
Endpoint: https://grok.com/rest/app-chat/conversations/new
Request:
{
"temporary": true,
"modelName": "grok-3",
"message": "https://imagine-public.x.ai/imagine-public/images/2d8260d7-3b7c-4bc1-84ca-6e82dd892c39.png alien walks down the hallway --mode=custom",
"toolOverrides": {
"videoGen":true
}
}
@@ -0,0 +1,4 @@
It looks like this requests generates the video and opens a long-lived, long-polling endpoint
that delivers lots of JSON blobs, eventually that contain the final video ID.
Before this is called, the "create media post" endpoint is called.
@@ -0,0 +1,189 @@
use crate::client::browser_user_agents::FIREFOX_143_MAC_USER_AGENT;
use crate::error::grok_client_error::GrokClientError;
use crate::error::grok_error::GrokError;
use crate::requests::upload_file::grok_upload_file_response::GrokUploadFileResponse;
use crate::requests::upload_file::request::UploadFileRequest;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use log::{error, info};
use std::path::Path;
use std::time::Duration;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONTENT_TYPE, COOKIE, ORIGIN, PRAGMA, REFERER, USER_AGENT};
use wreq::Client;
use wreq_util::Emulation;
const UPLOAD_FILE_URL : &str = "https://grok.com/rest/app-chat/upload-file";
/// Try to prevent buffer reallocations.
/// There's a better way to implement this.
const INITIAL_BUFFER_SIZE : usize = 1024*1024;
pub struct GrokUploadFile<'a, P: AsRef<Path>> {
pub file: FileSpec<'a, P>,
pub cookie: String,
pub request_timeout: Option<Duration>,
}
pub enum FileSpec<'a, P: AsRef<Path>> {
/// Path to the file
Path(P),
/// Bytes of the file
Bytes {
/// Must be owned.
bytes: &'a [u8],
/// Must be owned.
filename: String,
/// Mime type
mimetype: String,
}
}
impl <'a, P> GrokUploadFile<'a, P> where P: AsRef<Path> {
pub async fn upload(&self) -> Result<GrokUploadFileResponse, GrokError> {
match &self.file {
FileSpec::Path(path) => {
self.upload_from_file(path.as_ref()).await
}
FileSpec::Bytes { bytes, filename, mimetype } => {
unimplemented!("todo: implement bytes upload")
}
}
}
async fn upload_from_file(&self, file_path: &Path) -> Result<GrokUploadFileResponse, GrokError> {
let mut file = File::open(file_path)
.await
.map_err(|err| {
error!("Failed to open file for upload: {}", err);
GrokClientError::CannotOpenLocalFileForUpload(err)
})?;
let mut buffer = Vec::with_capacity(INITIAL_BUFFER_SIZE);
file.read_to_end(&mut buffer)
.await
.map_err(|err| {
error!("Failed to read file for upload: {}", err);
GrokClientError::CannotReadLocalFileForUpload(err)
})?;
let filename = file_path
.file_name()
.ok_or_else(|| GrokClientError::FileForUploadHasInvalidPath)?
.to_string_lossy()
.to_string();
let maybe_ext = file_path
.extension()
.and_then(|e| e.to_str());
// TODO: Share library code.
// TODO: Read file magic bytes first, then fall back to this.
let mime_type = match maybe_ext {
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("png") => "image/png",
// Some("webp") => "image/webp",
// Some("gif") => "image/gif",
// Some("mp4") => "video/mp4",
// Some("mov") => "video/quicktime",
// Some("webm") => "video/webm",
_ => "application/octet-stream",
};
// The encoded images have '/' and '+'.
// TODO: Padding or no?
let base64_file = BASE64_STANDARD.encode(buffer);
self.do_upload(&base64_file, &filename, &mime_type ).await
}
async fn do_upload(&self, base64_file: &str, file_name: &str, mime_type: &str) -> Result<GrokUploadFileResponse, GrokError> {
let client = Client::builder()
.emulation(Emulation::Firefox143)
.build()
.map_err(|err| GrokClientError::WreqClientError(err))?;
info!("Configuring client...");
// TODO: Headers were from Chromium, not Firefox. Partial implementation.
let mut request_builder = client.post(UPLOAD_FILE_URL)
.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())
.header(ORIGIN, "https://grok.com")
.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...");
//let response = builder.send()
// .await
// .map_err(|err| GrokClientError::WreqClientError(err))?;
if let Some(timeout) = self.request_timeout {
request_builder = request_builder.timeout(timeout);
}
/*
let request_body = UploadFileRequest {
fileName: file_name.to_string(),
fileMimeType: mime_type.to_string(),
content: base64_file,
fileSource: "IMAGINE_SELF_UPLOAD_FILE_SOURCE".to_string(),
};
let http_request = request_builder.json(&request_body)
.build()
.map_err(|err| {
error!("Error building Sora image generation HTTP request: {:?}", err);
SoraClientError::WreqClientError(err)
})?;
let response = client.execute(http_request)
.await
.map_err(|err| {
error!("Error during Sora image generation request: {:?}", err);
SoraClientError::WreqClientError(err)
})?;
let status = response.status();
let response_body = &response.text().await
.map_err(|err| {
error!("Error reading Sora image generation response body: {:?}", err);
SoraClientError::WreqClientError(err)
})?;
if !status.is_success() {
error!("Sora image generation request returned an error (code {}) : {:?}", status.as_u16(), response_body);
return Err(classify_general_http_status_code_and_body(status, response_body));
}
let response : HttpCreateResponse = serde_json::from_str(response_body)
.map_err(|err| SoraGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
*/
unimplemented!("todo")
}
}
@@ -0,0 +1,5 @@
/// Internal response structure
pub struct GrokUploadFileResponse {
}
@@ -0,0 +1,3 @@
mod grok_upload_file;
mod grok_upload_file_response;
mod request;
@@ -0,0 +1,18 @@
use serde::Serialize;
#[derive(Serialize)]
pub (super) struct UploadFileRequest {
#[serde(rename="fileName")]
pub fileName: String,
#[serde(rename="fileMimeType")]
pub fileMimeType: String,
/// Base64-encoded content
pub content: String,
/// eg. 'IMAGINE_SELF_UPLOAD_FILE_SOURCE'
#[serde(rename="fileSource")]
pub fileSource: String,
}
@@ -0,0 +1,26 @@
use std::fs::write;
use base64::Engine;
use base64::prelude::{BASE64_STANDARD, BASE64_STANDARD_NO_PAD};
use errors::AnyhowResult;
#[test]
fn test_pad() -> AnyhowResult<()> {
let buffer = get_base64_file();
let base64_file = BASE64_STANDARD.decode(buffer)?;
write("file_pad.jpg", &base64_file)?;
Ok(())
}
#[test]
fn test_no_pad() -> AnyhowResult<()> {
let buffer = get_base64_file();
let base64_file = BASE64_STANDARD_NO_PAD.decode(buffer)?;
write("file_no_pad.jpg", &base64_file)?;
Ok(())
}
fn get_base64_file() -> &'static str {
"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 Line truncated
}