Compare commits

...
Author SHA1 Message Date
Brandon Thomas 1957bd32c7 tools for image gen 2025-10-30 17:09:02 -04:00
Brandon Thomas 9d161aae97 include request id 2025-10-30 16:48:38 -04:00
Brandon Thomas 977ea223cd tool to poll images 2025-10-30 16:38:52 -04:00
Brandon Thomas 25ed1a0cdf video download client 2025-10-30 15:48:07 -04:00
Brandon Thomas b396d95f9a parse video token and url 2025-10-30 15:26:53 -04:00
Brandon Thomas 33d4264db5 cleanup logging 2025-10-30 15:10:31 -04:00
Brandon Thomas 397409c60b debug logging instead of print 2025-10-30 15:00:53 -04:00
Brandon Thomas 9386fbbc86 cleanup 2025-10-30 14:45:10 -04:00
Brandon Thomas 6f16f9f832 cleanup client of dead weight 2025-10-30 14:33:14 -04:00
Brandon Thomas 14bc3f80c8 refactor 2025-10-30 14:27:52 -04:00
Brandon Thomas b7e635bc0b refactor 2025-10-30 14:11:58 -04:00
Brandon Thomas e45f74617c refactor 2025-10-30 13:23:57 -04:00
Brandon Thomas cc9089388f parse email address from index 2025-10-29 04:32:28 -04:00
Brandon Thomas a906bcf1be fetch user id 2025-10-29 04:26:40 -04:00
Brandon Thomas e92bf8c0cb add auto-liking 2025-10-29 03:38:27 -04:00
Brandon Thomas 245f193b4a fix request 2025-10-29 03:01:07 -04:00
Brandon Thomas e15880a078 updated request, not generating video 2025-10-29 02:45:30 -04:00
Brandon Thomas 8cc42de9e1 wip 2025-10-29 02:33:55 -04:00
Brandon Thomas 41cebf6f54 wip 2025-10-29 02:14:20 -04:00
Brandon Thomas 32d7cfb620 refactor 2025-10-29 02:04:45 -04:00
Brandon Thomas 829944068d wip, refactor 2025-10-29 01:30:49 -04:00
Brandon Thomas 03f34755c6 refactor 2025-10-29 01:14:26 -04:00
Brandon Thomas 3d46e8d4b6 refactor 2025-10-29 01:05:42 -04:00
Brandon Thomas 44dce4eb7f reorganize 2025-10-29 00:57:51 -04:00
Brandon Thomas 4d89ce130b wip 2025-10-29 00:48:20 -04:00
Brandon Thomas 6d19931943 refactor, stronger types 2025-10-29 00:37:13 -04:00
Brandon Thomas 1c99dfa48f stronger types 2025-10-29 00:12:03 -04:00
Brandon Thomas ad7f7928f5 cleanup 2025-10-28 22:14:48 -04:00
Brandon Thomas 27b400724c cleanup 2025-10-28 21:44:27 -04:00
Brandon Thomas 28ab25b4d3 extract sentry trace, new recipe for getting all ingredients 2025-10-28 21:40:13 -04:00
Brandon Thomas ab5f6e184b video wip 2025-10-28 20:54:58 -04:00
Brandon Thomas 81a6264154 timestamp is testable 2025-10-28 20:32:00 -04:00
Brandon Thomas d3269d185f working 2025-10-28 20:26:34 -04:00
Brandon Thomas 0eb2f9f79c wip 2025-10-28 20:12:48 -04:00
Brandon Thomas b7739295a8 xs works 2025-10-28 19:13:15 -04:00
Brandon Thomas 9ea6109bdf test cases from python 2025-10-28 19:07:41 -04:00
Brandon Thomas 0096ba5136 sig_to_hex needs tests 2025-10-28 19:03:02 -04:00
Brandon Thomas cd569002c9 wip on sig_to_hex 2025-10-28 18:55:25 -04:00
Brandon Thomas 1d1ab51baa matches 2025-10-28 18:18:57 -04:00
Brandon Thomas 15d5596cdf signature style working with test case 2025-10-28 18:05:58 -04:00
Brandon Thomas 8f044a79e1 helpers 2025-10-28 17:50:00 -04:00
Brandon Thomas 7ddfea4c74 trig 2025-10-28 17:44:19 -04:00
Brandon Thomas 87f3d0c5f2 color 2025-10-28 17:36:22 -04:00
Brandon Thomas ad984c9adf another test case 2025-10-28 17:24:53 -04:00
Brandon Thomas 8541de78bf bezier working, needs more tests 2025-10-28 17:22:31 -04:00
Brandon Thomas 91eac7e940 bezier wip 2025-10-28 17:18:33 -04:00
Brandon Thomas d9770440b9 stylecp matches python 2025-10-28 17:01:33 -04:00
Brandon Thomas c60819e995 test case 2025-10-28 16:54:56 -04:00
Brandon Thomas 2f39ebd192 test 2 2025-10-28 16:53:16 -04:00
Brandon Thomas 7197e4eb23 test cases 2025-10-28 16:51:37 -04:00
Brandon Thomas f63aab5d3f one test case works 2025-10-28 15:39:36 -04:00
Brandon Thomas d71eea3ad3 simulate style wip 2025-10-28 15:12:26 -04:00
Brandon Thomas b85dd82c92 wip 2025-10-28 15:02:33 -04:00
Brandon Thomas 4e001bc89d cleanup 2025-10-28 14:59:01 -04:00
Brandon Thomas e4a748f664 unit test xa 2025-10-28 14:56:25 -04:00
Brandon Thomas 213c1e9283 xa clean 2025-10-28 14:36:35 -04:00
Brandon Thomas 4acdc0f9f6 xs c 2025-10-28 14:18:10 -04:00
Brandon Thomas fb8422ce5f xs idx 2025-10-28 14:09:40 -04:00
Brandon Thomas f294f71fdb algo match so far 2025-10-28 06:23:42 -04:00
Brandon Thomas 722c031a09 refactor / cleanup for legibility 2025-10-28 04:23:52 -04:00
Brandon Thomas 6bac6aaab8 baggage 2025-10-28 04:16:10 -04:00
Brandon Thomas 10428cd182 path and data 2025-10-28 03:37:30 -04:00
Brandon Thomas 4453b1a774 svg paths 2025-10-28 03:17:18 -04:00
Brandon Thomas 59ce976c31 numbers parsing 2025-10-28 02:55:55 -04:00
Brandon Thomas dadf17ebcc rename 2025-10-28 01:18:22 -04:00
Brandon Thomas ad0c2d0cfb actions and xsid 2025-10-28 01:12:55 -04:00
Brandon Thomas bbc3e78378 actions capture works 2025-10-27 20:54:55 -04:00
Brandon Thomas c57cad23df get index page and scripts 2025-10-27 20:09:14 -04:00
Brandon Thomas 68e2c8e67c fix fetching 2025-10-27 19:56:27 -04:00
Brandon Thomas 3e903972b2 script fetching wip 2025-10-27 19:38:32 -04:00
Brandon Thomas 3af8ddd602 parse scripts 2025-10-27 18:40:11 -04:00
Brandon Thomas 29b2acdaf8 anim 2025-10-27 18:20:10 -04:00
Brandon Thomas d821a2d6de use of parsing 2025-10-27 17:46:55 -04:00
Brandon Thomas 91fdbe2a83 working captures 2025-10-27 17:20:04 -04:00
Brandon Thomas df2c2b781f requests, a few x crypto libraries from github 2025-10-24 10:48:01 -04:00
Brandon Thomas c0450f1aaa video gen shut down due to anti bot rules 2025-10-22 13:11:58 -04:00
Brandon Thomas 45c91b03a9 working media post 2025-10-22 13:00:28 -04:00
Brandon Thomas ac6377ec21 video urls 2025-10-22 12:29:11 -04:00
Brandon Thomas b0c68b7f5d url generation 2025-10-22 12:26:08 -04:00
Brandon Thomas 9f600a4a85 file upload works 2025-10-22 00:41:50 -04:00
Brandon Thomas a85a5693f9 upload file wip 2025-10-22 00:23:34 -04:00
Brandon Thomas 515f2b9c28 more requests 2025-10-21 20:21:06 -04:00
Brandon Thomas 0b4a5e938c more requests 2025-10-21 20:02:34 -04:00
Brandon Thomas 2d44d667af requests 2025-10-21 19:54:43 -04:00
Brandon Thomas 6854243fd4 request metadata 2025-10-21 19:38:33 -04:00
Brandon Thomas b4dafa5926 messages were floats 2025-10-21 19:14:14 -04:00
Brandon Thomas c8152ce364 test 2025-10-21 18:58:57 -04:00
Brandon Thomas e7575e2c57 why does pretty formatting matter 2025-10-21 18:55:53 -04:00
Brandon Thomas f7246daf9e wire message is unknown - why?? 2025-10-21 18:51:11 -04:00
Brandon Thomas 658328a0b1 not pulling correct values over the wire 2025-10-21 18:46:11 -04:00
Brandon Thomas 75df3cc64c test cases 2025-10-21 17:46:19 -04:00
Brandon Thomas f38f3f9000 wip 2025-10-21 03:04:58 -04:00
Brandon Thomas c949cd18d5 more fields 2025-10-21 02:11:07 -04:00
Brandon Thomas d89dd1431c image data 2025-10-21 02:07:53 -04:00
Brandon Thomas 5a0726e472 json tagging 2025-10-21 02:02:41 -04:00
Brandon Thomas 05c6108e88 refactor 2025-10-21 01:40:41 -04:00
Brandon Thomas fa68dfced8 this does it! 2025-10-21 00:50:53 -04:00
Brandon Thomas 47041e6bd2 wip trying to poll with timeout 2025-10-21 00:38:44 -04:00
Brandon Thomas a9ce5ff2f6 messages 2025-10-20 23:46:03 -04:00
Brandon Thomas d5f102deac prompting wip 2025-10-20 23:40:47 -04:00
Brandon Thomas de24b2115a prompt images wip 2025-10-20 23:30:05 -04:00
Brandon Thomas 6fa4c9f2ec also handle 401 2025-10-20 17:33:00 -04:00
Brandon Thomas 93d2f203a8 cleanup 2025-10-20 17:27:15 -04:00
Brandon Thomas 290edce1a3 cleanup, new lib 2025-10-20 17:02:06 -04:00
Brandon Thomas b578dc81df IT WORKS! 2025-10-20 01:08:21 -04:00
Brandon Thomas 93f5181252 wip 2025-10-20 00:58:31 -04:00
Brandon Thomas 6721a0ed2b cloudflare recognizes lower case 1.1 headers 2025-10-19 02:10:27 -04:00
Brandon Thomas ac8d61fbee it knows this is fake - why? 2025-10-19 02:09:00 -04:00
Brandon Thomas 7283adf320 wip 2025-10-19 02:05:08 -04:00
Brandon Thomas 87c5a463b1 need to adjust cookies and header casing 2025-10-19 01:44:37 -04:00
Brandon Thomas bce628bdac wip 2025-10-19 01:34:02 -04:00
Brandon Thomas 8c0358009a still not working wtf 2025-10-18 23:21:55 -04:00
Brandon Thomas d3c2a0da47 wip 2025-10-18 23:15:41 -04:00
Brandon Thomas 6f800fd92b wip 2025-10-18 23:04:17 -04:00
Brandon Thomas 2f06aff623 wip 2025-10-18 22:23:19 -04:00
Brandon Thomas d258ea4081 wip 2025-10-18 22:18:34 -04:00
Brandon Thomas 958015d33f mitm proxy wip 2025-10-18 22:15:12 -04:00
Brandon Thomas 0c74568080 details on wreq 2025-10-18 21:58:38 -04:00
Brandon Thomas be1e1d78b0 mitm proxy details 2025-10-18 21:48:22 -04:00
Brandon Thomas feda3314c3 notes 2025-10-18 00:31:44 -04:00
Brandon Thomas 820ab35527 wreq ff 143 not much different than 139 2025-10-18 00:08:26 -04:00
Brandon Thomas c13a7bc52a testing 2025-10-17 23:57:35 -04:00
Brandon Thomas 253e5e4287 wip 2025-10-17 23:50:03 -04:00
Brandon Thomas 79f5bfdf26 browser fingerprints 2025-10-17 23:13:18 -04:00
Brandon Thomas c4b1e85f54 cleanup 2025-10-17 15:19:36 -04:00
Brandon Thomas 2f62e1fd3b wip 2025-10-17 15:18:54 -04:00
Brandon Thomas a5962d0ceb wip 2025-10-17 15:14:29 -04:00
Brandon Thomas 937d0848d1 note 2025-10-17 14:57:24 -04:00
Brandon Thomas 8f7ef3d942 curl-impersonate now works (in current form) 2025-10-17 14:55:31 -04:00
Brandon Thomas 4d22be15b1 current test script 2025-10-17 14:45:48 -04:00
Brandon Thomas 8395b946f8 notes 2025-10-17 14:17:33 -04:00
Brandon Thomas 8678017156 still wip 2025-10-17 12:57:40 -04:00
Brandon Thomas b8409607fd still not working 2025-10-17 12:11:05 -04:00
Brandon Thomas 27680b6760 logging util 2025-10-17 11:41:08 -04:00
Brandon Thomas a06b35ae74 logger things not helping 2025-10-17 11:37:16 -04:00
Brandon Thomas 77c01d6170 headers 2025-10-17 10:45:59 -04:00
Brandon Thomas a3a3425bbc notes 2025-10-17 10:41:48 -04:00
Brandon Thomas 87d8304cc8 still broken 2025-10-17 10:13:01 -04:00
Brandon Thomas 82952e240e wip 2025-10-17 10:09:59 -04:00
Brandon Thomas d4473b852c something is wrong with websocket 2025-10-17 03:41:45 -04:00
Brandon Thomas d0fc1b47d6 account block 2025-10-17 03:15:54 -04:00
Brandon Thomas cbe2a2a6d5 ugh, user-agent header was scuffed 2025-10-17 03:15:37 -04:00
Brandon Thomas 89286ba459 tauri window wip 2025-10-17 02:26:01 -04:00
Brandon Thomas 512059c0f5 wip 2025-10-17 01:49:34 -04:00
Brandon Thomas 6af238c48e wip 2025-10-17 01:06:33 -04:00
Brandon Thomas 201dcb0192 wreq websocket wip 2025-10-17 00:50:29 -04:00
Brandon Thomas b867df290a grok wip 2025-10-17 00:12:32 -04:00
Brandon Thomas 851aba7e06 midjourney task completion data 2025-10-16 20:29:09 -04:00
Brandon Thomas 8ff9323ae6 add metadata to sora uploads 2025-10-16 20:14:16 -04:00
Brandon Thomas 50b085d450 minor cleanup 2025-10-16 19:17:04 -04:00
Brandon Thomas 1b70ee7394 fixup sora sentinel tokens (#879) 2025-10-16 19:09:52 -04:00
Brandon Thomas 1c3a176705 fix docs compile errors 2025-10-15 21:18:46 -04:00
bflatastic 70c305f78d promptbox clickthrough fix 3d 2025-10-15 22:49:47 +07:00
Brandon Thomas 1c3178d073 task queue api - thumbnails, tokens, etc. (#878) 2025-10-15 02:51:08 -04:00
Tanish Manku 1243e755fe Add reset ability to 2D (#874) 2025-10-14 15:57:03 -07:00
292 changed files with 13899 additions and 518 deletions
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
"describe": {
"columns": [
{
@@ -49,18 +49,43 @@
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"name": "on_complete_primary_media_file_token",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_class",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "on_complete_batch_token",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_cdn_url",
"ordinal": 12,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_thumbnail_url_template",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"ordinal": 14,
"type_info": "Int64"
},
{
"name": "updated_at: DateTime<Utc>",
"ordinal": 10,
"ordinal": 15,
"type_info": "Int64"
},
{
"name": "completed_at: DateTime<Utc>",
"ordinal": 11,
"ordinal": 16,
"type_info": "Int64"
}
],
@@ -77,10 +102,15 @@
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true
]
},
"hash": "a142dc3fdf5b6e741c268d8f2e9996a28c214f5835681297d58a9431da8de939"
"hash": "47f94c127abc20683b25c257aed3c2190b9bd29eb829d61392f08e10fd986767"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "50a2449b87abed7df3302a74ecd0e8ab87940077a247636680cb09b2fe488321"
}
Generated
+456 -115
View File
File diff suppressed because it is too large Load Diff
+11 -2
View File
@@ -12,6 +12,7 @@ members = [
"crates/api_clients/fal_client",
"crates/api_clients/google_drive_common",
"crates/api_clients/google_sign_in",
"crates/api_clients/grok_client",
"crates/api_clients/midjourney_client",
"crates/api_clients/openai_sora_client",
"crates/api_clients/openart_client",
@@ -43,6 +44,7 @@ members = [
"crates/lib/caching/memory_store",
"crates/lib/caching/redis_caching",
"crates/lib/cloudflare_errors",
"crates/lib/cloudflare_mitigation",
"crates/lib/concurrency",
"crates/lib/cookie_store",
"crates/lib/cookies",
@@ -84,6 +86,7 @@ members = [
"crates/lib/testing",
"crates/lib/tts_common",
"crates/lib/url_config",
"crates/lib/url_utils",
"crates/lib/user_input_common",
"crates/lib/web_scrapers",
#"crates/lib/api_clients/fakeyou_client",
@@ -185,6 +188,7 @@ artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
build_metadata = { path = "crates/lib/build_metadata" }
cloud_storage = { path = "crates/api_clients/cloud_storage" }
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
cloudflare_mitigation = { path = "crates/lib/cloudflare_mitigation" }
component_traits = { path = "crates/service/plugins/component_traits" }
cookie_store = { path = "crates/lib/cookie_store" }
crockford = { path = "crates/lib/data/crockford" }
@@ -210,6 +214,7 @@ subprocess_common = { path = "crates/lib/subprocess_common" }
test_data = { path = "crates/lib/test_data" }
testing = { path = "crates/lib/testing" }
tokens = { path = "crates/schema/public/tokens" }
url_utils = { path = "crates/lib/url_utils" }
web_base64 = { path = "crates/lib/data/web_base64" }
# External libs
@@ -230,19 +235,22 @@ base64 = "0.22.1"
bytes = "1.10.1"
chrono = { version = "0.4.41", features = ["serde"] }
cookie = "0.18.1"
env = "1.0.1"
env_logger = "0.11.8"
futures = { version = "0.3.31" }
hex-literal = "1.0.0"
indexmap = "2.9.0"
log = "0.4.27"
once_cell = "1.21.3"
rand = "0.9.1"
regex = "1.11.1"
regex = "1.12.2"
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
reqwest-websocket = { version = "0.5.0" }
serde = { version = "1.0.219", features = ["derive"] }
serde_derive = "1.0.219"
serde_json = "1.0.140"
serde_yaml = "0.9.34+deprecated"
sha2 = "0.11.0-rc.2"
sha3 = "0.10.8"
tauri = { version = "2.8.3", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
tauri-plugin-dialog = "2.3.3"
@@ -255,6 +263,7 @@ thiserror = "2.0.12"
tokio = { version = "1.45.1", features = ["macros"] }
toml = "0.8.22"
url = { version = "2.5.4", features = ["serde"] }
uuid = "1.18.1"
# NB: wreq gets past fingerprinting, but we have to deal with platform-specific build issues.
#
@@ -266,5 +275,5 @@ url = { version = "2.5.4", features = ["serde"] }
# choco install pkgconfiglite -y
# choco install llvm -y
# choco install nasm -y
wreq = { version = "6.0.0-rc.20", features = ["json", "multipart", "cookies"] }
wreq = { version = "6.0.0-rc.20", features = ["json", "multipart", "cookies", "ws"] }
wreq-util = "3.0.0-rc.5"
@@ -10,6 +10,13 @@
-- tasks_v2.sqlite - added model_type (nullable)
-- tasks_v3.sqlite - added frontend_caller, comments
-- tasks_v4.sqlite - added is_dismissed_by_user
-- tasks_v5.sqlite - added five fields:
-- on_complete_batch_token,
-- on_complete_primary_media_file_token,
-- on_complete_primary_media_file_class,
-- on_complete_primary_media_file_cdn_url,
-- on_complete_primary_media_file_thumbnail_url_template,
--
CREATE TABLE tasks (
-- Task auto-incrementing primary key.
@@ -54,6 +61,29 @@ CREATE TABLE tasks (
-- Whether the user has dismissed the task from view.
is_dismissed_by_user INTEGER NOT NULL DEFAULT 0,
-- OPTIONAL.
-- When the generation completes, the batch token (if any).
-- This will point to a collection of multiple files.
on_complete_batch_token TEXT,
-- OPTIONAL.
-- When the generation completes, the media token of the "primary" media file.
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
on_complete_primary_media_file_token TEXT,
-- OPTIONAL.
-- When the generation completes, the file type of the primary media file.
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
on_complete_primary_media_file_class TEXT,
-- OPTIONAL.
-- The URL to the media file.
on_complete_primary_media_file_cdn_url TEXT,
-- OPTIONAL.
-- The URL template for the thumbnail of the primary media file.
on_complete_primary_media_file_thumbnail_url_template TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
completed_at INTEGER DEFAULT NULL
@@ -6,4 +6,5 @@ pub mod jobs;
pub mod media_file;
pub mod prompts;
pub mod stripe_artcraft;
pub mod subscriptions;
pub mod subscriptions;
pub mod utils;
@@ -0,0 +1,12 @@
use crate::common::responses::media_links::MediaLinks;
/// Get the thumbnail template for either images or videos.
pub fn media_links_to_thumbnail_template(links: &MediaLinks) -> Option<&str> {
// NB: We only populate video previews for video tasks.
if let Some(video) = links.maybe_video_previews.as_ref() {
return Some(&video.animated_thumbnail_template);
}
// Image case - use the image thumbnail template if it exists.
links.maybe_thumbnail_template.as_deref()
}
@@ -0,0 +1 @@
pub mod media_links_to_thumbnail_template;
+51
View File
@@ -0,0 +1,51 @@
[package]
name = "grok_client"
edition = "2024"
version = "0.0.1"
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "grok_client"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration.workspace = true
# Internal
cloudflare_errors.workspace = true
cloudflare_mitigation.workspace = true
cookie_store.workspace = true
errors.workspace = true
# External
anyhow.workspace = true
base64.workspace = true
byteorder = "1.5.0"
chrono.workspace = true
futures.workspace = true
log.workspace = true
once_cell.workspace = true
regex.workspace = true
scraper = "0.24.0"
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
tokio = { version = "1", features = ["full"] }
url.workspace = true
uuid.workspace = true
wreq-util.workspace = true
wreq.workspace = true
[dev-dependencies]
filesys.workspace = true
test_data.workspace = true
testing.workspace = true
log4rs = "1.4.0"
env_logger.workspace = true
env.workspace = true
chrono.workspace = true
@@ -0,0 +1,10 @@
/// Firefox 139 (2025) on macOS, which matches wreq's Emulation::Firefox139
pub const FIREFOX_139_MAC_USER_AGENT : &str =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0";
pub const FIREFOX_143_MAC_USER_AGENT : &str =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0";
pub const FIREFOX_144_MAC_USER_AGENT_UNSUPPORTED_TLS_FINGERPRINTING : &str =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0";
@@ -0,0 +1 @@
pub mod browser_user_agents;
@@ -0,0 +1,517 @@
#!/usr/bin/env python3
# From https://github.com/kbtit25/grok2api/blob/73c917b15438e12699531ccf9411d3a7d1110cbf/xStatsigIDGenerator.py
# -*- coding: utf-8 -*-
import base64
import struct
import hashlib
import time
import secrets
import requests
import re
import json
from typing import Optional, Dict, Any
class XStatsigIDGenerator:
"""x-statsig-id 生成器"""
def __init__(self):
self.base_timestamp = int(time.time()) # 使用当前系统时间
self.grok_url = "https://grok.com"
def get_grok_meta_content(self) -> bytes:
"""
从 grok.com 获取 meta 标签中的 grok-site-verification 内容
使用多种方法彻底解决403问题
Returns:
48字节的meta内容
"""
print("🌐 正在请求 grok.com...")
# 定义多种绕过策略
strategies = [
#self._try_curl_with_proxy,
self._try_curl_with_different_ua,
#self._try_requests_with_session,
#self._try_curl_cffi_advanced,
#self._try_alternative_endpoints,
#self._try_cached_content
]
for i, strategy in enumerate(strategies):
try:
print(f" 尝试策略 {i+1}: {strategy.__name__}")
result = strategy()
if result:
return result
except Exception as e:
print(f" 策略 {i+1} 失败: {e}")
continue
# 所有策略都失败,使用备用内容
print(" ❌ 所有策略都失败,使用备用meta内容")
fallback = b"backup-grok-meta-content-when-request-fails-ok"
return fallback + b'\x00' * (48 - len(fallback))
def _try_curl_with_proxy(self) -> bytes:
"""策略1: 使用curl + 代理"""
import subprocess
# 尝试多个公共代理
proxies = [
None, # 无代理
"socks5://127.0.0.1:1080", # 本地代理
"http://127.0.0.1:8080", # 本地HTTP代理
]
for proxy in proxies:
try:
curl_command = [
'curl', '-s', '-L', '--max-time', '15',
'--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'--header', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'--header', 'Accept-Language: en-US,en;q=0.5',
'--header', 'Accept-Encoding: gzip, deflate',
'--header', 'Connection: keep-alive',
'--header', 'Upgrade-Insecure-Requests: 1',
'--compressed'
]
if proxy:
curl_command.extend(['--proxy', proxy])
curl_command.append(self.grok_url)
result = subprocess.run(curl_command, capture_output=True, text=True, timeout=15)
if result.returncode == 0 and len(result.stdout) > 1000:
print(f" ✅ curl成功 (代理: {proxy or '无'})")
return self._extract_meta_from_html(result.stdout)
except Exception:
continue
return None
def _try_curl_with_different_ua(self) -> bytes:
"""策略2: 使用不同的User-Agent"""
import subprocess
user_agents = [
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0',
#'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
#'Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/109.0 Firefox/119.0',
#'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
#'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15'
]
for ua in user_agents:
try:
curl_command = [
'curl-impersonate-ff', '-s', '-L', '--max-time', '10',
'--user-agent', ua,
'--header', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'--header', 'Accept-Language: en-US,en;q=0.9',
'--header', 'Cache-Control: no-cache',
'--header', 'Pragma: no-cache',
'--compressed',
self.grok_url
]
result = subprocess.run(curl_command, capture_output=True, text=True, timeout=10)
if result.returncode == 0 and len(result.stdout) > 1000:
print(f" ✅ 不同UA成功")
return self._extract_meta_from_html(result.stdout)
except Exception:
continue
return None
def _try_requests_with_session(self) -> bytes:
"""策略3: 使用requests session模拟真实浏览器行为"""
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
# 配置重试策略
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 模拟真实浏览器行为
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Cache-Control': 'max-age=0'
}
try:
# 先访问主页建立session
session.get('https://x.com', headers=headers, timeout=5)
# 再访问grok
response = session.get(self.grok_url, headers=headers, timeout=10)
if response.status_code == 200 and len(response.text) > 1000:
print(f" ✅ session请求成功")
return self._extract_meta_from_html(response.text)
except Exception:
pass
return None
def _try_curl_cffi_advanced(self) -> bytes:
"""策略4: 使用curl_cffi高级模拟"""
try:
from curl_cffi import requests as curl_requests
# 尝试不同的浏览器模拟
impersonations = ['chrome120', 'chrome119', 'safari17', 'firefox119']
for imp in impersonations:
try:
response = curl_requests.get(
self.grok_url,
impersonate=imp,
timeout=10,
headers={
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Cache-Control': 'no-cache'
}
)
if response.status_code == 200 and len(response.text) > 1000:
print(f" ✅ curl_cffi成功 ({imp})")
return self._extract_meta_from_html(response.text)
except Exception:
continue
except ImportError:
pass
return None
def _try_alternative_endpoints(self) -> bytes:
"""策略5: 尝试替代端点"""
import subprocess
# 尝试不同的URL路径
alternative_urls = [
'https://grok.com/',
'https://grok.com/login',
'https://grok.com/home',
'https://x.com/i/grok'
]
for url in alternative_urls:
try:
curl_command = [
'curl', '-s', '-L', '--max-time', '8',
'--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'--header', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'--compressed',
url
]
result = subprocess.run(curl_command, capture_output=True, text=True, timeout=8)
if result.returncode == 0 and len(result.stdout) > 500:
print(f" ✅ 替代端点成功: {url}")
meta_result = self._extract_meta_from_html(result.stdout)
if meta_result:
return meta_result
except Exception:
continue
return None
def _try_cached_content(self) -> bytes:
"""策略6: 使用缓存或预设内容"""
# 如果有真实的grok meta内容,可以在这里硬编码
known_meta_contents = [
"grok-site-verification-content-2024-production-v1",
"x-grok-verification-meta-content-stable-release",
"grok-meta-verification-string-for-api-access"
]
for content in known_meta_contents:
try:
meta_bytes = content.encode('utf-8')
if len(meta_bytes) < 48:
meta_bytes = meta_bytes + b'\x00' * (48 - len(meta_bytes))
elif len(meta_bytes) > 48:
meta_bytes = meta_bytes[:48]
print(f" ✅ 使用预设内容: {content[:30]}...")
return meta_bytes
except Exception:
continue
return None
def _extract_meta_from_html(self, html_content: str) -> bytes:
"""从HTML中提取meta内容"""
patterns = [
r'<meta\s+name=["\']grok-site-verification["\']\s+content=["\']([^"\']+)["\']',
r'<meta\s+content=["\']([^"\']+)["\']\s+name=["\']grok-site-verification["\']',
r'grok-site-verification["\']?\s*(?:content|value)\s*=\s*["\']([^"\']+)["\']',
# 扩展模式,查找其他可能的meta标签
r'<meta\s+name=["\']verification["\']\s+content=["\']([^"\']+)["\']',
r'<meta\s+name=["\']site-verification["\']\s+content=["\']([^"\']+)["\']'
]
for pattern in patterns:
match = re.search(pattern, html_content, re.IGNORECASE)
if match:
verification_content = match.group(1)
print(f" ✅ 找到meta内容: {verification_content[:50]}...")
# 转换为48字节
meta_bytes = verification_content.encode('utf-8')
if len(meta_bytes) < 48:
meta_bytes = meta_bytes + b'\x00' * (48 - len(meta_bytes))
elif len(meta_bytes) > 48:
meta_bytes = meta_bytes[:48]
return meta_bytes
# 如果没找到特定的meta标签,尝试从HTML中提取其他有用信息
if 'grok' in html_content.lower() and len(html_content) > 1000:
# 使用HTML内容的哈希作为meta内容
import hashlib
content_hash = hashlib.sha256(html_content.encode()).hexdigest()[:48]
meta_bytes = content_hash.encode('utf-8')
if len(meta_bytes) < 48:
meta_bytes = meta_bytes + b'\x00' * (48 - len(meta_bytes))
print(f" ✅ 使用内容哈希作为meta: {content_hash[:32]}...")
return meta_bytes
return None
def generate_browser_fingerprint(self) -> str:
"""
生成浏览器指纹信息 (结合方法1和方法3)
Returns:
指纹字符串
"""
print("🔍 生成浏览器指纹...")
# 模拟浏览器指纹信息
fingerprint_data = {
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0",
"language": "en",
"languages": ["en", "en-US"],
"platform": "MacIntel",
"cookieEnabled": True,
"doNotTrack": None,
"screenWidth": 450,
"screenHeight": 654,
"screenColorDepth": 24,
"screenPixelDepth": 24,
"screenAvailWidth": 450,
"screenAvailHeight": 654,
"innerWidth": 450,
"innerHeight": 654,
"outerWidth": 1920,
"outerHeight": 1055,
"timezone": "America/New York",
"timezoneOffset": -400,
"hardwareConcurrency": 14,
"deviceMemory": 16,
"maxTouchPoints": 0
}
# 方法3: 生成指纹哈希
fingerprint_string = json.dumps(fingerprint_data, sort_keys=True, separators=(',', ':'))
fingerprint_hash = hashlib.sha256(fingerprint_string.encode('utf-8')).hexdigest()
print(f" 指纹数据长度: {len(fingerprint_string)} 字符")
print(f" 指纹哈希: {fingerprint_hash[:32]}...")
return fingerprint_hash
def generate_x_statsig_id(self, method: str = "GET", pathname: str = "/") -> str:
"""
生成完整的 x-statsig-id
Args:
method: 请求方式 (GET/POST)
pathname: 请求路径
Returns:
生成的 x-statsig-id 字符串
"""
print("=" * 60)
print("🚀 开始生成 x-statsig-id")
print("=" * 60)
print(f"📋 生成参数:")
print(f" Method: {method}")
print(f" Pathname: {pathname}")
# 1. 获取 grok.com 的 meta content
meta_content = self.get_grok_meta_content()
# 2. 生成浏览器指纹
fingerprint = self.generate_browser_fingerprint()
# 3. 生成当前时间戳
current_timestamp = int(time.time())
relative_timestamp = current_timestamp - self.base_timestamp
print(f"⏰ 时间信息:")
print(f" 当前时间戳: {current_timestamp}")
print(f" 基准时间戳: {self.base_timestamp}")
print(f" 相对时间戳: {relative_timestamp}")
# 4. 生成时间戳字节 (小端序)
timestamp_bytes = struct.pack('<I', relative_timestamp)
print(f" 时间戳字节: {timestamp_bytes.hex()}")
# 5. 生成SHA256
sha_input = f"{method}!{pathname}!{relative_timestamp}{fingerprint}"
sha256_hash = hashlib.sha256(sha_input.encode('utf-8')).digest()
sha256_16bytes = sha256_hash[:16]
print(f"🔐 SHA256信息:")
print(f" 输入字符串: {sha_input[:100]}...")
print(f" SHA256前16字节: {sha256_16bytes.hex()}")
# 6. 固定值
fixed_byte = b'\x03'
# 7. 组合payload数据
payload_data = meta_content + timestamp_bytes + sha256_16bytes + fixed_byte
print(f"📦 Payload长度: {len(payload_data)} 字节")
# 8. 生成异或key并加密
xor_key = secrets.randbits(8)
encrypted_payload = bytes([b ^ xor_key for b in payload_data])
print(f"🔑 异或信息:")
print(f" 异或key: 0x{xor_key:02x} ({xor_key})")
# 9. 组合最终数据
final_data = bytes([xor_key]) + encrypted_payload
print(f" 最终数据长度: {len(final_data)} 字节")
# 10. Base64编码
result = base64.b64encode(final_data).decode('utf-8')
print(f"✅ 生成结果:")
print(f" x-statsig-id: {result}")
print(f" 长度: {len(result)} 字符")
return result
def verify_generated_id(self, statsig_id: str) -> bool:
"""
验证生成的ID结构是否正确
Args:
statsig_id: 要验证的ID
Returns:
验证是否通过
"""
print("\n" + "=" * 60)
print("🔍 验证生成的ID结构")
print("=" * 60)
try:
# Base64解码
decoded_bytes = base64.b64decode(statsig_id)
print(f"✅ Base64解码成功,长度: {len(decoded_bytes)} 字节")
# 提取异或key
xor_key = decoded_bytes[0]
print(f"✅ 异或key: 0x{xor_key:02x} ({xor_key})")
# 异或解密
decrypted = bytearray()
for i in range(1, len(decoded_bytes)):
decrypted.append(decoded_bytes[i] ^ xor_key)
print(f"✅ 解密后长度: {len(decrypted)} 字节")
# 验证数据结构
expected_length = 48 + 4 + 16 + 1 # meta + timestamp + sha256 + fixed
if len(decrypted) == expected_length:
print(f"✅ 数据长度正确: {len(decrypted)}/{expected_length}")
else:
print(f"❌ 数据长度错误: {len(decrypted)}/{expected_length}")
return False
# 检查固定值
fixed_val = decrypted[-1]
if fixed_val == 3:
print(f"✅ 固定值正确: {fixed_val}")
else:
print(f"❌ 固定值错误: {fixed_val} (期望: 3)")
return False
# 解析时间戳
timestamp_bytes = decrypted[48:52]
timestamp = struct.unpack('<I', timestamp_bytes)[0]
actual_time = self.base_timestamp + timestamp
print(f"✅ 时间戳解析:")
print(f" 相对时间: {timestamp} 秒")
print(f" 绝对时间: {actual_time}")
print(f" 时间差: {abs(time.time() - actual_time):.1f} 秒")
print("🎉 ID结构验证通过!")
return True
except Exception as e:
print(f"❌ 验证失败: {e}")
return False
def main():
"""主函数 - 演示完整流程"""
generator = XStatsigIDGenerator()
# 生成 x-statsig-id
method = "POST"
pathname = "/rest/app-chat/conversations/new"
statsig_id = generator.generate_x_statsig_id(method, pathname)
print("Generated id", statsig_id)
# 验证生成的ID
generator.verify_generated_id(statsig_id)
print(f"\n" + "=" * 60)
print("📋 使用说明")
print("=" * 60)
print("在HTTP请求中使用:")
print(f"Headers: {{'x-statsig-id': '{statsig_id}'}}")
if __name__ == "__main__":
main()
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
# From https://github.com/realasfngl/Grok-Api/blob/d8f7c6754d95ad3707b214f93930ee454724035d/core/xctid.py
from math import floor, copysign, pi, cos, sin
from base64 import b64decode, b64encode
from re import findall, sub
from typing import List, Dict
from random import random
from hashlib import sha256
from struct import pack
from time import time
class Signature:
@staticmethod
def _h(x: float, _param: float, c: float, e: bool):
f = ((x * (c - _param)) / 255.0) + _param
if e:
return floor(f)
rounded = round(float(f), 2)
if rounded == 0.0:
return 0.0
return rounded
@staticmethod
def cubicBezierEased(t: float, x1: float, y1: float, x2: float, y2: float) -> float:
def bezier(u: float):
omu = 1.0 - u
b1 = 3.0 * omu * omu * u
b2 = 3.0 * omu * u * u
b3 = u * u * u
x = b1 * x1 + b2 * x2 + b3
y = b1 * y1 + b2 * y2 + b3
return x, y
lo, hi = 0.0, 1.0
for _ in range(80):
mid = 0.5 * (lo + hi)
if bezier(mid)[0] < t:
lo = mid
else:
hi = mid
u = 0.5 * (lo + hi)
return bezier(u)[1]
@staticmethod
def xa(svg: str) -> List[List[int]]:
s = (svg)
substr = s[9:]
parts = substr.split("C")
out = []
for part in parts:
cleaned = sub(r"[^\d]+", " ", part).strip()
if cleaned == "":
nums = [0]
else:
nums = [int(tok) for tok in cleaned.split() if tok != ""]
out.append(nums)
return out
@staticmethod
def tohex(num: float) -> str:
rounded = round(float(num), 2)
if rounded == 0.0:
return "0"
sign = "-" if copysign(1.0, rounded) < 0 else ""
absval = abs(rounded)
intpart = int(floor(absval))
frac = absval - intpart
if frac == 0.0:
return sign + format(intpart, "x")
frac_digits = []
f = frac
for _ in range(20):
f *= 16
digit = int(floor(f + 1e-12))
frac_digits.append(format(digit, "x"))
f -= digit
if abs(f) < 1e-12:
break
frac_str = "".join(frac_digits).rstrip("0")
if frac_str == "":
return sign + format(intpart, "x")
return sign + format(intpart, "x") + "." + frac_str
@staticmethod
def simulateStyle(values: List[int], c: int) -> Dict[str,str]:
duration = 4096
currentTime = round(c / 10.0) * 10
t = currentTime / duration
cp = [Signature._h(v, -1 if (i % 2) else 0, 1, False) for i, v in enumerate(values[7:])]
easedY = Signature.cubicBezierEased(t, cp[0], cp[1], cp[2], cp[3])
start = [float(x) for x in values[0:3]]
end = [float(x) for x in values[3:6]]
r = round(start[0] + (end[0] - start[0]) * easedY)
g = round(start[1] + (end[1] - start[1]) * easedY)
b = round(start[2] + (end[2] - start[2]) * easedY)
color = f"rgb({r}, {g}, {b})"
endAngle = Signature._h(values[6], 60, 360, True)
angle = endAngle * easedY
rad = angle * pi / 180.0
def is_effectively_zero(val: float) -> bool:
return abs(val) < 1e-7
def is_effectively_integer(val: float) -> bool:
return abs(val - round(val)) < 1e-7
cosv = cos(rad)
sinv = sin(rad)
if is_effectively_zero(cosv):
a = 0
d = 0
else:
if is_effectively_integer(cosv):
a = int(round(cosv))
d = int(round(cosv))
else:
a = f"{cosv:.6f}"
d = f"{cosv:.6f}"
if is_effectively_zero(sinv):
bval = 0
cval = 0
else:
if is_effectively_integer(sinv):
bval = int(round(sinv))
cval = int(round(-sinv))
else:
bval = f"{sinv:.7f}"
cval = f"{(-sinv):.7f}"
transform = f"matrix({a}, {bval}, {cval}, {d}, 0, 0)"
return {"color": color, "transform": transform}
@staticmethod
def xs(x_bytes: bytes, svg: str, x_values: list) -> str:
arr = list(x_bytes)
idx = arr[x_values[0]] % 16
c = ((arr[x_values[1]] % 16) * (arr[x_values[2]] % 16)) * (arr[x_values[3]] % 16)
o = Signature.xa(svg)
vals = o[idx]
k = Signature.simulateStyle(vals, c)
concat = str(k["color"]) + str(k["transform"])
matches = findall(r"[\d\.\-]+", concat)
converted = []
for m in matches:
num = float(m)
hexstr = Signature.tohex(num)
converted.append(hexstr)
joined = "".join(converted)
cleaned = joined.replace(".", "").replace("-", "")
return cleaned
@staticmethod
def generate_sign(path: str, method: str, verification: str, svg: str, x_values: list, time_n: int = None, random_float: float = None) -> str:
n = int(time() - 1682924400) if not time_n else time_n
t = pack('<I', n)
r = b64decode(verification)
o = Signature.xs(r, svg, x_values)
msg = "!".join([method, path, str(n)]) + "obfiowerehiring" + o
digest = sha256(msg.encode('utf-8')).digest()[:16]
prefix_byte = int(floor(random() if not random_float else random_float * 256))
assembled = bytes([prefix_byte]) + r + t + digest + bytes([3])
arr = bytearray(assembled)
if len(arr) > 0:
first = arr[0]
for i in range(1, len(arr)):
arr[i] = arr[i] ^ first
return b64encode(bytes(arr)).decode('ascii').replace('=', '')
def main():
sig = Signature.generate_sign('/rest/app-chat/conversations/new', 'POST', self.verification_token, self.svg_data, self.numbers)
print('Signature', sig)
pass
if __name__ == 'main':
main()
@@ -0,0 +1,8 @@
https://grok.com/ --> https://accounts.x.ai/sign-in?redirect=grok-com
--> Success --> https://grok.com/
https://grok.com/imagine
@@ -0,0 +1,36 @@
use crate::datatypes::api::baggage::Baggage;
use crate::datatypes::api::sentry_trace::SentryTrace;
use crate::datatypes::api::svg_path_data::SvgPathData;
use crate::datatypes::api::user_email::UserEmail;
use crate::datatypes::api::user_id::UserId;
use crate::datatypes::api::verification_token::VerificationToken;
use crate::datatypes::api::xsid_numbers::XsidNumbers;
#[derive(Clone)]
pub struct GrokClientSecrets {
/// Baggage is <meta> tag data from the index.html
pub baggage: Baggage,
/// Baggage is <meta> tag data from the index.html
pub sentry_trace: SentryTrace,
/// Baggage is <meta> tag data from the index.html
pub verification_token: VerificationToken,
/// The SVG path data to use (chosen via verification_token)
/// We selected one of four-ish possible SVG paths (of length >= 200) by
/// the `verification token -> animation index` algo.
pub svg_path_data: SvgPathData,
/// This doesn't come from the index.html
/// These numbers are separately loaded from the xsid javascript
pub numbers: XsidNumbers,
/// From index.html
/// Not strictly needed to sign requests, but typically needed to generate URLs.
pub user_id: UserId,
/// From index.html
/// Not needed, but returned alongside other details.
pub user_email: Option<UserEmail>,
}
@@ -0,0 +1,24 @@
#[derive(Clone)]
pub struct GrokCookies {
/// The entire cookies header (for now)
cookies: String,
}
impl GrokCookies {
pub fn new(cookies: String) -> Self {
Self { cookies }
}
pub fn as_str(&self) -> &str {
&self.cookies
}
pub fn as_bytes(&self) -> &[u8] {
self.cookies.as_bytes()
}
pub fn to_string(&self) -> String {
self.cookies.clone()
}
}
@@ -0,0 +1,25 @@
use crate::credentials::grok_client_secrets::GrokClientSecrets;
use crate::credentials::grok_cookies::GrokCookies;
/// Includes the cookies and all the magic other bits needed to call the API.
#[derive(Clone)]
pub struct GrokFullCredentials {
/// Entire cookie payload
pub cookies: GrokCookies,
/// Secrets from `index.html` and the xsid javascript.
/// Required for video generation.
pub client_secrets: GrokClientSecrets,
}
impl GrokFullCredentials {
pub fn from_cookies_and_client_secrets(
cookies: GrokCookies,
client_secrets: GrokClientSecrets
) -> Self {
Self {
cookies,
client_secrets,
}
}
}
@@ -0,0 +1,3 @@
pub mod grok_cookies;
pub mod grok_full_credentials;
pub mod grok_client_secrets;
@@ -0,0 +1,4 @@
/// "baggage" data from the <meta> tags
#[derive(Debug, Clone)]
pub struct Baggage(pub(crate) String);
@@ -0,0 +1,5 @@
/// Type for File IDs.
/// File IDs are UUIds.
#[derive(Clone, Debug)]
pub struct FileId(pub String);
@@ -0,0 +1,9 @@
pub mod baggage;
pub mod file_id;
pub mod request_id;
pub mod sentry_trace;
pub mod svg_path_data;
pub mod user_email;
pub mod user_id;
pub mod verification_token;
pub mod xsid_numbers;
@@ -0,0 +1,7 @@
/// Request IDs.
/// Type for image websocket "tasks"
/// These are for image generation websockets.
/// Request IDs are UUIds.
#[derive(Clone, Debug)]
pub struct RequestId(pub String);
@@ -0,0 +1,40 @@
use uuid::Uuid;
/// "sentry trace" data from the <meta> tags
#[derive(Debug, Clone)]
pub struct SentryTrace(pub (crate) String);
impl SentryTrace {
/// Generate the format of trace sent in the HTTP request headers
pub fn to_http_request_header(&self) -> String {
// Python reference:
// f'{self.sentry_trace}-{str(uuid4()).replace("-", "")[:16]}-0',
// Inner bit:
// str(uuid4()).replace("-", "")[:16]
let sentry_start = &self.0;
let sentry_uuid = Uuid::new_v4().to_string();
let sentry_inner = sentry_uuid.replace("-", "")[..16].to_string();
format!("{sentry_start}-{sentry_inner}-0")
}
}
#[cfg(test)]
mod tests {
use crate::datatypes::api::sentry_trace::SentryTrace;
#[test]
fn test_real_trace() {
let short = SentryTrace("d6d7c55e4a489c0dabaed06e5c7b257b".to_string());
let header = short.to_http_request_header();
assert_eq!(header.len(), 51);
}
#[test]
fn test_invalid_trace() {
let short = SentryTrace("".to_string());
let header = short.to_http_request_header();
assert_eq!(header.len(), 19); // header is invalid, we're just making sure Rust doesn't panic
}
}
@@ -0,0 +1,4 @@
/// Data on an SVG path, eg. <svg d={data} />
#[derive(Debug, Clone)]
pub struct SvgPathData(pub(crate) String);
@@ -0,0 +1,3 @@
/// Type for User Emails
#[derive(Clone, Debug)]
pub struct UserEmail(pub String);
@@ -0,0 +1,5 @@
/// Type for User IDs.
/// User IDs are UUIDs.
#[derive(Clone, Debug)]
pub struct UserId(pub String);
@@ -0,0 +1,3 @@
#[derive(Debug, Clone)]
pub struct VerificationToken(pub(crate) String);
@@ -0,0 +1,6 @@
/// Magic numbers pulled from Javascript
#[derive(Clone, Debug)]
pub struct XsidNumbers {
pub numbers: Vec<u32>,
}
@@ -0,0 +1,18 @@
use std::path::Path;
/// Descriptor of a file to be uploaded
/// Includes necessary metadata
pub enum FileUploadSpec<P: AsRef<Path>> {
/// Path to the file
Path(P),
/// Bytes of the file
Bytes {
/// Must be owned.
bytes: Vec<u8>,
/// Must be owned.
filename: String,
/// Mime type
mimetype: String,
}
}
@@ -0,0 +1,2 @@
pub mod api;
pub mod file_upload_spec;
@@ -0,0 +1,122 @@
use base64::DecodeError;
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum GrokClientError {
/// An error was encountered in building the Wreq client
WreqClientError(wreq::Error),
/// Error serializing a message to send the websocket
WebsocketRequestSerializationError(serde_json::Error),
/// Error locking the websocket for sending/receiving
WebsocketLockError,
/// Error reading from a websocket.
WebsocketReadError(wreq::Error),
/// 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,
/// Error parsing HTML
HtmlParsingError,
/// Error decoding verification token
FailedToDecodeVerificationToken(DecodeError),
/// Verification token bytes are invalid
InvalidVerificationTokenBytes,
/// Our script logic is out of date
ScriptLogicOutOfDate,
/// Signature algorithm isn't working with inputs
BadSignatureInputs,
/// Something is broken with timeout math
TimeoutMathBroken,
// /// An error reading the file for upload.
// FileForUploadReadError(std::io::Error),
//
// /// The file path provided for upload is invalid.
// FileForUploadHasInvalidPath,
//
// /// Something is wrong with the JWT bearer token.
// /// This error originates on our end as we try to parse the JWT.
// LocalJwtClaimsParseError(String),
//
// /// There was an error constructing the form-multipart request.
// MultipartFormError(wreq::Error),
//
// /// We haven't received a bearer token yet
// /// This is our own internal application state error, not something Sora returns.
// /// We know our client can't make the request, so we preemptively fail it.
// NoBearerTokenForRequest,
//
// /// A sentinel token is not present in the client, which is required for some requests.
// NoSentinelTokenForRequest,
//
// /// Issue with using the SoraCredentialBuilder.
// SoraCredentialBuilderError(&'static str),
//
// /// Error parsing a request URL.
// UrlParseError(url::ParseError),
//
// /// Error serializing the sentinel store token to JSON (typically for persistent storage).
// CouldNotSerializeSentinelTokenStore(serde_json::Error),
//
// /// Error deserializing the sentinel token from JSON (typically for persistent storage).
// CouldNotDeserializeSentinelTokenStore {
// error: serde_json::Error,
// raw_json: String
// },
}
impl Error for GrokClientError {}
impl Display for GrokClientError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err),
Self::WebsocketRequestSerializationError(err) => write!(f, "Websocket request serialization error: {}", err),
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::HtmlParsingError => write!(f, "Html parsing error"),
Self::FailedToDecodeVerificationToken(err) => write!(f, "Failed to decode verification token: {}", err),
Self::InvalidVerificationTokenBytes => write!(f, "Invalid verification token bytes"),
Self::ScriptLogicOutOfDate => write!(f, "Script logic out of date"),
Self::BadSignatureInputs => write!(f, "Bad signature inputs"),
Self::TimeoutMathBroken => write!(f, "Timeout math is broken"),
//Self::FileForUploadReadError(err) => write!(f, "Error reading file for upload: {}", err),
//Self::FileForUploadHasInvalidPath => write!(f, "The file path provided for upload is invalid."),
//Self::LocalJwtClaimsParseError(msg) => write!(f, "Local JWT claims parse error: {}", msg),
//Self::MultipartFormError(err) => write!(f, "Multipart form error: {}", err),
//Self::NoBearerTokenForRequest => write!(f, "No bearer token available. The client needs a bearer token to make the request."),
//Self::NoSentinelTokenForRequest => write!(f, "No sentinel token available. The client needs a sentinel token to make the request."),
//Self::SoraCredentialBuilderError(msg) => write!(f, "Sora Credential Builder error: {}", msg),
//Self::UrlParseError(err) => write!(f, "URL parse error: {}", err),
//Self::CouldNotSerializeSentinelToken(err) => write!(f, "Could not serialize sentinel token to JSON: {}", err),
//Self::CouldNotSerializeSentinelTokenStore(err) => write!(f, "Could not serialize sentinel token store to JSON: {}", err),
//Self::CouldNotDeserializeSentinelTokenStore { error, raw_json} => {
// write!(f, "Could not deserialize sentinel token store from JSON: {:?}, raw JSON: {}", error, raw_json)
//},
}
}
}
@@ -0,0 +1,53 @@
use crate::error::grok_client_error::GrokClientError;
use crate::error::grok_generic_api_error::GrokGenericApiError;
use crate::error::grok_specific_api_error::GrokSpecificApiError;
use cloudflare_errors::cloudflare_error::CloudflareError;
use std::error::Error;
#[derive(Debug)]
pub enum GrokError {
Client(GrokClientError),
ApiSpecific(GrokSpecificApiError),
ApiGeneric(GrokGenericApiError),
}
impl GrokError {
pub fn is_grok_having_downtime_issues(&self) -> bool {
match self {
Self::ApiGeneric(GrokGenericApiError::CloudflareError(CloudflareError::BadGateway502)) => true,
Self::ApiGeneric(GrokGenericApiError::CloudflareError(CloudflareError::GatewayTimeout504)) => true,
Self::ApiGeneric(GrokGenericApiError::CloudflareError(CloudflareError::TimeoutOccurred524)) => true,
_ => false,
}
}
}
impl Error for GrokError {}
impl std::fmt::Display for GrokError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Client(e) => write!(f, "GrokClientError: {:?}", e),
Self::ApiSpecific(e) => write!(f, "GrokSpecificApiError: {:?}", e),
Self::ApiGeneric(e) => write!(f, "GrokGenericApiError: {:?}", e),
}
}
}
impl From<GrokClientError> for GrokError {
fn from(error: GrokClientError) -> Self {
Self::Client(error)
}
}
impl From<GrokSpecificApiError> for GrokError {
fn from(error: GrokSpecificApiError) -> Self {
Self::ApiSpecific(error)
}
}
impl From<GrokGenericApiError> for GrokError {
fn from(error: GrokGenericApiError) -> Self {
Self::ApiGeneric(error)
}
}
@@ -0,0 +1,78 @@
use cloudflare_errors::cloudflare_error::CloudflareError;
use std::error::Error;
use std::fmt::{Display, Formatter};
use wreq::StatusCode;
#[derive(Debug)]
pub enum GrokGenericApiError {
/// Specific Cloudflare errors.
CloudflareError(CloudflareError),
/// The downloaded index.html did not include the expected data.
/// This is necessary for handshakes between client and server.
IndexHtmlDidNotIncludeExpectedData {
message: String,
},
/// The downloaded javascript did not include the expected data.
/// This is necessary for handshakes between client and server.
ScriptBodyDidNotIncludeExpectedData {
message: String,
},
/// Error creating the websocket due to 401 Unauthorized, likely an authentication error with Grok.
/// Unfortunately we can't read the response body text to diagnose more because wreq has some API design issues.
LikelyWebsocketAuthentication401,
/// We received a 403 on websocket upgrade, likely from Cloudflare.
/// Unfortunately we can't read the response body text to diagnose more because wreq has some API design issues.
LikelyWebsocketCloudflare403,
/// Unknown status code on websocket upgrade.
UnexpectedWebsocketUpgradeStatusCode(StatusCode),
/// serde_json::Error, likely from JSON deserialization schema mismatch.
/// Includes the original body.
SerdeResponseParseErrorWithBody(serde_json::Error, String),
/// serde_json::Error, likely from JSON deserialization schema mismatch.
/// Includes the original body.
/// Specifically, this is for non-200 (error) responses.
SerdeParseErrorWithBodyOnNon200(serde_json::Error, String),
/// An uncategorized bad HTTP response from Grok.
UncategorizedBadResponseWithStatusAndBody {
status_code: StatusCode,
body: String,
},
/// Our previous upload failed
UploadFailed,
/// An error upgrading the connection to a websocket.
WreqWebsocketUpgradeError(wreq::Error),
/// An uncaught error from the API client.
WreqError(wreq::Error),
}
impl Error for GrokGenericApiError {}
impl Display for GrokGenericApiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err),
Self::IndexHtmlDidNotIncludeExpectedData { message } => write!(f, "IndexHtmlDidNotIncludeExpectedData ( message: {} )", message),
Self::ScriptBodyDidNotIncludeExpectedData { message } => write!(f, "ScriptBodyDidNotIncludeExpectedData ( message: {} )", message),
Self::LikelyWebsocketAuthentication401 => write!(f, "Likely websocket authentication 401 error (probably Grok authentication issue with cookies)"),
Self::LikelyWebsocketCloudflare403 => write!(f, "Likely Cloudflare 403 error on websocket upgrade"),
Self::UnexpectedWebsocketUpgradeStatusCode(status) => write!(f, "Unexpected websocket upgrade status code: {}", status),
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
Self::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 response body: {:?}. Body: {}", err, body),
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body),
Self::UploadFailed => write!(f, "Upload failed"),
Self::WreqWebsocketUpgradeError(err) => write!(f, "Websocket upgrade error: {}", err),
Self::WreqError(err) => write!(f, "Wreq client error: {}", err),
}
}
}
@@ -0,0 +1,72 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum GrokSpecificApiError {
TodoError,
// /// Failed to generate a bearer token despite the `200 OK` response.
// /// This has been generated by sending old/expired session cookies or empty cookies.
// FailedToGenerateBearerToken,
//
// /// The endpoint told us we used an invalid JWT.
// InvalidJwt,
//
// /// Sora has responded with "sentinel block".
// /// This usually means we need to refresh our sentinel token.
// SentinelBlockError,
//
// /// Sora requires users to set up an account username before they can generate images.
// /// This requires the user to set up that username.
// SoraUsernameNotYetCreated,
//
// /// Sora has responded with "token expired".
// /// This (probably?) means we need to refresh our JWT bearer token.
// TokenExpiredError,
//
// /// We're sending too many tasks to Sora.
// /// Sora is telling us to cool it.
// TooManyConcurrentTasks,
//
// // TODO: Try to make concrete error types for these distinct cases.
// /// Unauthorized, cookie and/or bearer token expired. We'll need to ask for a refreshed login.
// ///
// /// Example message, e.g. from the upload endpoint:
// /// {
// /// "error": {
// /// "message": "Your authentication token has expired. Please try signing in again.",
// /// "type": "invalid_request_error",
// /// "param": null,
// /// "code": "token_expired"
// /// }
// /// }
// UnauthorizedCookieOrBearerExpired,
//
// /// The Sora sentinel token response is missing fields
// SentinelResponseIsMissingFields {
// missing_token: bool,
// missing_turnstile: bool,
// raw_response: String,
// },
}
impl Error for GrokSpecificApiError {}
impl Display for GrokSpecificApiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::TodoError => write!(f, "TodoError"),
//Self::FailedToGenerateBearerToken => write!(f, "Failed to generate bearer token. Probably invalid or expired session cookies."),
//Self::InvalidJwt => write!(f, "Invalid JWT token. Please refresh the token."),
//Self::SentinelBlockError => write!(f, "Request blocked by Sentinel Token Block. Please refresh the sentinel token."),
//Self::SoraUsernameNotYetCreated => write!(f, "Sora username not yet created. Please create a username in the Sora app or website."),
//Self::TokenExpiredError => write!(f, "JWT token has expired. Please refresh the token."),
//Self::TooManyConcurrentTasks => write!(f, "Too many concurrent tasks. Please wait."),
//Self::UnauthorizedCookieOrBearerExpired => write!(f, "Unauthorized: cookie and/or bearer token expired"),
//Self::SentinelResponseIsMissingFields { missing_token: token_missing, missing_turnstile: turnstile_missing, raw_response } => {
// write!(f, "Sentinel response is missing fields. token_missing: {}, turnstile_missing: {}. Raw response: {}", token_missing, turnstile_missing, raw_response)
//},
}
}
}
@@ -0,0 +1,4 @@
pub mod grok_client_error;
pub mod grok_error;
pub mod grok_generic_api_error;
pub mod grok_specific_api_error;
+15
View File
@@ -0,0 +1,15 @@
//! A Grok API client.
#[cfg(test)]
pub (crate) mod test_utils;
// Library utils
pub (crate) mod client;
// User lib
pub mod credentials;
pub mod datatypes;
pub mod error;
pub mod recipes;
pub mod requests;
pub mod utils;
@@ -0,0 +1,93 @@
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
}
}
Header 'x-xai-request-id: 7cb93340-5fb6-4fb3-b88d-ff9f5d99d456' is just a random UUID and it can be changed.
Generate Image -> Video Flow
============================
(TODO)
2. Create Media Post
Request
{
"mediaType": "MEDIA_POST_TYPE_IMAGE",
"mediaUrl":"https://imagine-public.x.ai/imagine-public/images/2d8260d7-3b7c-4bc1-84ca-6e82dd892c39.png"
}
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,2 @@
pub mod request_client_secrets;
mod upload_image_and_generate_video;
@@ -0,0 +1,124 @@
use crate::credentials::grok_client_secrets::GrokClientSecrets;
use crate::credentials::grok_cookies::GrokCookies;
use crate::error::grok_error::GrokError;
use crate::error::grok_generic_api_error::GrokGenericApiError;
use crate::requests::index_page::get_index_page_and_scripts::{get_index_page_and_scripts, GetIndexPageAndScriptsArgs};
use crate::requests::index_page::get_xsid_script::{get_xsid_script, GetXsidScriptArgs};
use crate::requests::index_page::parsers::index::parse_index_baggage::parse_index_baggage;
use crate::requests::index_page::parsers::index::parse_index_sentry_trace::parse_index_sentry_trace;
use crate::requests::index_page::parsers::index::parse_index_svg_paths::parse_svg_paths_from_index_html;
use crate::requests::index_page::parsers::index::parse_index_user_email::parse_index_user_email;
use crate::requests::index_page::parsers::index::parse_index_user_id::parse_index_user_id;
use crate::requests::index_page::parsers::index::parse_index_verification_token::parse_index_verification_token;
use crate::requests::index_page::parsers::script::parse_script_actions_and_xsid_script_path::parse_script_actions_and_xsid_script_path;
use crate::requests::index_page::parsers::script::parse_xsid_script_numbers::parse_xsid_script_numbers;
use crate::requests::index_page::utils::convert_verification_token_to_loading_anim::convert_verification_token_to_loading_anim;
use crate::requests::index_page::utils::select_svg_path_by_loading_anim::select_svg_path_by_loading_anim;
pub struct RequestClientSecretsArgs<'a> {
pub cookies: &'a GrokCookies,
}
// Video generation needs:
// 1) baggage -- on index page
// 2) sentry_trace -- on index page
// 3) x_statsig
/// Load all the things needed to make requests.
pub async fn request_client_secrets(args: RequestClientSecretsArgs<'_>) -> Result<GrokClientSecrets, GrokError> {
let payloads = get_index_page_and_scripts(GetIndexPageAndScriptsArgs {
cookie: args.cookies.as_str(),
}).await?;
let baggage = parse_index_baggage(&payloads.index_body_html)
.ok_or_else(|| GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
message: "Index did not include baggage.".to_string()
})?;
let sentry_trace = parse_index_sentry_trace(&payloads.index_body_html)
.ok_or_else(|| GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
message: "Index did not include sentry trace.".to_string()
})?;
let verification_token = parse_index_verification_token(&payloads.index_body_html)
.ok_or_else(|| GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
message: "Index did not include verification token.".to_string()
})?;
let user_id = parse_index_user_id(&payloads.index_body_html)
.ok_or_else(|| GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
message: "Index did not include user id.".to_string()
})?;
// NB: Optional.
let maybe_user_email = parse_index_user_email(&payloads.index_body_html);
// NB: We'll only use one of the several SVG paths
let svg_paths = parse_svg_paths_from_index_html(&payloads.index_body_html);
if svg_paths.is_empty() {
return Err(GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
message: "Index did not include any SVG paths.".to_string(),
}.into());
}
let loading_anim = convert_verification_token_to_loading_anim(&verification_token)?;
let svg_path_data = select_svg_path_by_loading_anim(&svg_paths, &loading_anim)?;
let actions_and_xsid_script = parse_script_actions_and_xsid_script_path(&payloads.scripts)?;
let xsid_script = get_xsid_script(GetXsidScriptArgs {
client: &payloads.client,
html: &payloads.index_body_html,
cookie: args.cookies.as_str(),
xsid_script_id: &actions_and_xsid_script.xsid_script_path,
}).await?;
let numbers = parse_xsid_script_numbers(&xsid_script.xsid_script_body);
if numbers.numbers.is_empty() {
return Err(GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
message: "Index did not include any SVG paths.".to_string(),
}.into());
}
Ok(GrokClientSecrets {
baggage,
sentry_trace,
verification_token,
svg_path_data,
numbers,
user_id,
user_email: maybe_user_email,
})
}
#[cfg(test)]
mod tests {
use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
use errors::AnyhowResult;
#[tokio::test]
#[ignore] // Manual test invocation
async fn test() -> AnyhowResult<()> {
let cookies = get_typed_test_cookies()?;
let secrets = request_client_secrets(RequestClientSecretsArgs {
cookies: &cookies,
}).await?;
println!("Baggage: {:?}", secrets.baggage);
println!("Sentry trace: {:?}", secrets.sentry_trace);
println!("Verification token: {:?}", secrets.verification_token);
println!("SVG path: {:?}", secrets.svg_path_data);
println!("Numbers: {:?}", secrets.numbers);
println!("User ID: {:?}", secrets.user_id);
println!("User Email: {:?}", secrets.user_email);
assert_eq!(1, 2);
Ok(())
}
}
@@ -0,0 +1,180 @@
use crate::credentials::grok_full_credentials::GrokFullCredentials;
use crate::datatypes::api::file_id::FileId;
use crate::datatypes::file_upload_spec::FileUploadSpec;
use crate::error::grok_error::GrokError;
use crate::error::grok_generic_api_error::GrokGenericApiError;
use crate::requests::create_media_post::grok_create_media_post::{GrokCreateMediaPost, MediaPostType};
use crate::requests::like_media::grok_like_media::GrokLikeMediaPost;
use crate::requests::upload_file::grok_upload_file::GrokUploadFile;
use crate::requests::video_chat::grok_video_gen_chat_conversation::{GrokVideoGenChatConversationBuilder, VideoMediaPostType};
use crate::utils::user_and_file_id_to_image_url::user_and_file_id_to_image_url;
use crate::utils::user_and_file_id_to_video_url::user_and_file_id_to_video_url;
use log::{error, info};
use std::path::Path;
use std::time::Duration;
pub struct UploadImageAndGenerateVideo<'a, P: AsRef<Path>> {
pub full_credentials: &'a GrokFullCredentials,
// NB: Must be owned.
pub file: FileUploadSpec<P>,
pub prompt: Option<&'a str>,
pub individual_request_timeout: Option<Duration>,
}
pub struct ImageUploadResult {
pub video_file_id: Option<FileId>,
pub video_url: Option<String>,
}
pub async fn upload_image_and_generate_video<P: AsRef<Path>>(args: UploadImageAndGenerateVideo<'_, P>) -> Result<ImageUploadResult, GrokError> {
info!("Uploading file to Grok...");
let request = GrokUploadFile {
file: args.file,
cookie: args.full_credentials.cookies.to_string(),
request_timeout: args.individual_request_timeout,
};
let upload_result = request.upload().await?;
info!("File URI: {:?}", upload_result.file_uri);
info!("File Metadata ID: {:?}", upload_result.file_id);
let upload_file_id = match &upload_result.file_id {
Some(id) => id,
None => {
error!("Previous file upload failed. Cannot continue.");
return Err(GrokGenericApiError::UploadFailed.into());
}
};
let url = user_and_file_id_to_image_url(
&args.full_credentials.client_secrets.user_id,
&upload_file_id
);
info!("Uploaded URI: {:?}", url);
info!("Creating media post...");
let request = GrokCreateMediaPost {
user_id: &args.full_credentials.client_secrets.user_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?;
info!("Posted Image.");
info!("Generate video...");
let request = GrokVideoGenChatConversationBuilder {
user_id: &args.full_credentials.client_secrets.user_id,
file_id: &upload_file_id,
media_type: VideoMediaPostType::UserUploadedImage,
cookie: args.full_credentials.cookies.as_str(),
prompt: args.prompt,
request_timeout: args.individual_request_timeout,
baggage: &args.full_credentials.client_secrets.baggage,
sentry_trace: &args.full_credentials.client_secrets.sentry_trace,
verification_token: &args.full_credentials.client_secrets.verification_token,
svg_data: &args.full_credentials.client_secrets.svg_path_data,
numbers: &args.full_credentials.client_secrets.numbers,
};
// TODO: Get URL
let video_gen_result = request.send().await?;
let maybe_video_file_id = video_gen_result.video_file_id;
let maybe_video_url = maybe_video_file_id
.as_ref()
.map(|file_id| {
user_and_file_id_to_video_url(&args.full_credentials.client_secrets.user_id, file_id, false)
});
info!("Video Generation Enqueued");
info!("Liking media...");
let request = GrokLikeMediaPost {
file_id: &upload_file_id,
cookie: args.full_credentials.cookies.as_str(),
request_timeout: args.individual_request_timeout,
baggage: &args.full_credentials.client_secrets.baggage,
sentry_trace: &args.full_credentials.client_secrets.sentry_trace,
verification_token: &args.full_credentials.client_secrets.verification_token,
svg_data: &args.full_credentials.client_secrets.svg_path_data,
numbers: &args.full_credentials.client_secrets.numbers,
};
let _like_result = request.send().await?;
info!("Media Liked");
Ok(ImageUploadResult {
video_file_id: maybe_video_file_id,
video_url: maybe_video_url,
})
}
#[cfg(test)]
mod tests {
use crate::credentials::grok_full_credentials::GrokFullCredentials;
use crate::datatypes::file_upload_spec::FileUploadSpec;
use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
use crate::recipes::upload_image_and_generate_video::{upload_image_and_generate_video, UploadImageAndGenerateVideo};
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
use crate::test_utils::setup_test_logging::setup_test_logging;
use errors::AnyhowResult;
use log::LevelFilter;
// Result: GrokUploadFileResponse { file_metadata_id:
// Some("acdee48f-9d6f-4bc6-9d06-fcc97dd4418a"), file_uri:
// Some("users/85980643-ffab-4984-a3de-59a608c47d7f/acdee48f-9d6f-4bc6-9d06-fcc97dd4418a/content") }
#[tokio::test]
#[ignore] // Client side tests only
async fn test_upload_image_and_generate_video() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Info);
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";
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);
let credentials = GrokFullCredentials::from_cookies_and_client_secrets(cookies, secrets);
let video_result = upload_image_and_generate_video(UploadImageAndGenerateVideo {
full_credentials: &credentials,
file: FileUploadSpec::Path(image_path),
prompt: Some(prompt),
individual_request_timeout: None,
}).await?;
println!("Video File ID: {:?}", video_result.video_file_id);
println!("Video URL: {:?}", video_result.video_url);
assert_eq!(1, 2);
Ok(())
}
}
@@ -0,0 +1,23 @@
curl 'https://grok.com/rest/media/post/create' \
-H 'accept: */*' \
-H 'accept-language: en-US,en;q=0.9' \
-H 'baggage: sentry-environment=production,sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c,sentry-trace_id=e6364e990c1c6323d921be66f302ae4c,sentry-org_id=4508179396558848,sentry-sampled=false,sentry-sample_rand=0.1163701079375088,sentry-sample_rate=0' \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; cf_clearance=Olsa5S4XZRkpXnkTimScfA2BS2pAUhYUekBoGpl6wTM-1761089503-1.2.1.1-r0eoaGnqMSC6GzMQg3v4g3yuMTA_RPq1l8aegFvhmQwfsChLMc2yIO9RxeVBHX3pgRS.V3K8opP.fparwgv6QBrU4nCQLffH_CFXNVsyxloZUweZDAALRV1ga0q0BNGHT7eYK07c6q5nJyJgR0oKjpxp2d3ppxREr2_hdZFBloGpK9FIJMonfeZwQ6l4V7lOVpAZLqUiympOxKQ3ZjyrlBOxPyA0iyLGKH..tIBYKkE; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1761089504$o11$g1$t1761090040$j24$l0$h0' \
-H 'origin: https://grok.com' \
-H 'pragma: no-cache' \
-H 'priority: u=1, i' \
-H 'referer: https://grok.com/imagine/favorites' \
-H 'sec-ch-ua: "Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "macOS"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-origin' \
-H 'sentry-trace: e6364e990c1c6323d921be66f302ae4c-a7e2c94a5414642e-0' \
-H 'traceparent: 00-6d79c28e5adca0d7970dd7e9f8a025db-8e1df0c0e7a3d738-01' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36' \
-H 'x-statsig-id: mNwPce3OuHbH7v3kKUCnTly5oHilV0VAiIxmTcaaOoB4WfFMQZuCeMejHjcCmGu2khEuMJw71PWMT5hgHn4GTW8csx8kmw' \
-H 'x-xai-request-id: 51b30743-6092-49d5-abd8-c81d2325720f' \
--data-raw '{"mediaType":"MEDIA_POST_TYPE_IMAGE","mediaUrl":"https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content"}'
@@ -0,0 +1,2 @@
I'm not sure what this endpoint does.
It's right after file upload and right before video creation.
@@ -0,0 +1,13 @@
{
"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": []
}
}
@@ -0,0 +1,23 @@
curl 'https://grok.com/rest/media/post/create' \
-H 'accept: */*' \
-H 'accept-language: en-US,en;q=0.9' \
-H 'baggage: sentry-environment=production,sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c,sentry-trace_id=92cb1e6544b08682afb452f36c11510e,sentry-org_id=4508179396558848,sentry-sampled=false,sentry-sample_rand=0.742448150042881,sentry-sample_rate=0' \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=CM68n_6ICla70GH6wjh.ozDsswZDbqessR4kTZu1khM-1761091013-1.2.1.1-ZDDMukpbB18fA0E07Dqgmhv7XC5a7zhIu44YwDQ5Jl77rR9EcDcUN7sO3wU6XiHKmKRpCEkREkDF7tOjUJzYUcYQmah9wjbAoF349KM17MUWfIKvWQzYD1OaWqofZ2i8CyZvOqNxK8AHF.VFaDYtBGQXv555yG42Tu8IMF3YDwJHjE5bM0.ViZ09qvPKpGiPiO52G1Tf0uPEBqc448pzHqiA_S5gu_wrsRbJnLQ98vE; _ga_8FEWB057YH=GS2.1.s1761089504$o11$g1$t1761092041$j52$l0$h0' \
-H 'origin: https://grok.com' \
-H 'pragma: no-cache' \
-H 'priority: u=1, i' \
-H 'referer: https://grok.com/imagine/post/641a8afe-4a4a-42a8-9365-d3c3a79b7e32-5' \
-H 'sec-ch-ua: "Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "macOS"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-origin' \
-H 'sentry-trace: 92cb1e6544b08682afb452f36c11510e-bb7537b86d293d55-0' \
-H 'traceparent: 00-913b23480b60713f561081fb0ef2944c-c09587b2d626bdd6-01' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36' \
-H 'x-statsig-id: 4ilBgcYOzs+qrKV1fb2qIigWrUVpnpr3TCdvS7pX05Ce4t6XRIf6NdljDhOPFWepSYVcSuYR4DGnmcmQCyG59h7KuwxX4Q' \
-H 'x-xai-request-id: 6999e95e-a793-4a5e-89cd-eca027e125c8' \
--data-raw '{"mediaType":"MEDIA_POST_TYPE_IMAGE","mediaUrl":"https://imagine-public.x.ai/imagine-public/images/2d8260d7-3b7c-4bc1-84ca-6e82dd892c39.png"}'
@@ -0,0 +1,15 @@
{
"post": {
"id": "2d8260d7-3b7c-4bc1-84ca-6e82dd892c39",
"userId": "85980643-ffab-4984-a3de-59a608c47d7f",
"createTime": "2025-10-22T00:14:15.635290002Z",
"prompt": "an alien in the hallway",
"mediaType": "MEDIA_POST_TYPE_IMAGE",
"mediaUrl": "https://imagine-public.x.ai/imagine-public/images/2d8260d7-3b7c-4bc1-84ca-6e82dd892c39.png",
"mimeType": "image/png",
"audioUrls": [],
"childPosts": [],
"modelName": "imagine_h_1",
"thumbnailImageUrl": "https://imagine-public.x.ai/cdn-cgi/image/width=500,fit=scale-down,format=auto/imagine-public/images/2d8260d7-3b7c-4bc1-84ca-6e82dd892c39.png"
}
}
@@ -0,0 +1,159 @@
use crate::client::browser_user_agents::FIREFOX_143_MAC_USER_AGENT;
use crate::datatypes::api::file_id::FileId;
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::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};
use std::time::Duration;
use wreq::header::{ACCEPT, ACCEPT_LANGUAGE, CACHE_CONTROL, CONTENT_TYPE, COOKIE, ORIGIN, PRAGMA, REFERER, USER_AGENT};
use wreq::Client;
use wreq_util::Emulation;
const CREATE_MEDIA_POST_URL : &str = "https://grok.com/rest/media/post/create";
/// Request builder
pub struct GrokCreateMediaPost<'a> {
pub user_id: &'a UserId,
pub file_id: &'a FileId,
pub media_type: MediaPostType,
pub cookie: &'a str,
pub request_timeout: Option<Duration>,
}
#[derive(Debug, Copy, Clone)]
pub enum MediaPostType {
UserUploadedImage,
GrokGeneratedImage,
Video,
}
/// Response type
#[derive(Debug)]
pub struct GrokCreateMediaPostResponse {
}
impl <'a> GrokCreateMediaPost<'a> {
pub async fn send(&self) -> Result<GrokCreateMediaPostResponse, GrokError> {
info!("Configuring client...");
let client = Client::builder()
.emulation(Emulation::Firefox143)
.build()
.map_err(|err| GrokClientError::WreqClientError(err))?;
// TODO: Headers were from Chromium, not Firefox. Partial implementation.
let mut request_builder = client.post(CREATE_MEDIA_POST_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 media_url = match self.media_type {
MediaPostType::UserUploadedImage => user_and_file_id_to_image_url(self.user_id, self.file_id),
MediaPostType::GrokGeneratedImage => unimplemented!("implement for generated images"),
MediaPostType::Video => unimplemented!("implement for videos"),
};
let request_body = CreateMediaPostWireRequest {
media_type: "MEDIA_POST_TYPE_IMAGE".to_string(),
media_url,
};
let http_request = request_builder.json(&request_body)
.build()
.map_err(|err| {
error!("Error building create media request: {:?}", err);
GrokClientError::WreqClientError(err)
})?;
let response = client.execute(http_request)
.await
.map_err(|err| {
error!("Error during create media: {:?}", err);
GrokGenericApiError::WreqError(err)
})?;
let status = response.status();
let response_body = &response.text()
.await
.map_err(|err| {
error!("Error reading Grok create media response body: {:?}", err);
GrokGenericApiError::WreqError(err)
})?;
// TODO:
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()))?;
Ok(GrokCreateMediaPostResponse{
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::get_test_cookies::get_test_cookies;
use errors::AnyhowResult;
#[tokio::test]
#[ignore] // Client side tests only
async fn create_media_post() -> AnyhowResult<()> {
//setup_test_logging(LevelFilter::Trace);
let cookies = get_test_cookies()?;
let user_id = UserId("85980643-ffab-4984-a3de-59a608c47d7f".to_string()); // User
let file_id = FileId("990ddf90-8f34-42b1-81a5-39c509d62ff7".to_string()); // Mochi
let request = GrokCreateMediaPost {
user_id: &user_id,
file_id: &file_id,
media_type: MediaPostType::UserUploadedImage,
cookie: &cookies,
request_timeout: None,
};
let result = request.send().await?;
println!("Result: {:?}", result);
assert_eq!(1, 2);
Ok(())
}
}
@@ -0,0 +1,2 @@
pub mod grok_create_media_post;
pub (super) mod request;
@@ -0,0 +1,10 @@
use serde::Serialize;
#[derive(Serialize)]
pub (super) struct CreateMediaPostWireRequest {
#[serde(rename = "mediaType")]
pub (super) media_type: String,
#[serde(rename = "mediaUrl")]
pub (super) media_url: String,
}
@@ -0,0 +1,17 @@
// Note: this was accessing the file outside of the page (right click-> open in new tab)
curl 'https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/generated/34fa0313-3cde-4693-b046-319093374dbe/generated_video.mp4?cache=1' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-US,en;q=0.5' \
-H 'Accept-Encoding: gzip, deflate, br, zstd' \
-H 'Referer: https://grok.com/' \
-H 'Sec-GPC: 1' \
-H 'Connection: keep-alive' \
-H 'Cookie: cf_clearance=exXZNJVPTGE_jw9bkiCb3mhyiRhx4m9KU3dOAIg4jQQ-1761851143-1.2.1.1-ylhkr3kBkOwGLyhesgDutGoC515v.kuElqWRvO63KLtVpPOGiOJ8oXNZpCKHjFfB.jS6ucdE1QVGpW06pr_dKs1QWI5vLEhDc7elgHiaGGAFOrNb..raDuk0UCvlQtXS9Iqy5k9eUq.LJ9QptwsjbAMm2K1y8tACePxF.qcDUXeoRWvlAOvWU4dTIS5ZsU6c1BD5ujgWYujpANhcE93qosdLJ6ho7f5elwI0aOLbx0k; x-anonuserid=41f3614a-1a08-4c9b-81b4-0c2085f6b8d8; x-challenge=tmsr%2B%2BbUg2pjTWyAOGqqFe%2BW38g66vDImJiVuiLtmSmNpaQm%2B46WajkdQoflNC1CyRKRcOLOc1ylzYI%2BerCVbWLrHr022ZWcTJqcN0o1499%2FcjPAAspeAKPV%2F6OAaHmYGu8eSXR4dj27qXSWxLixSctJLIQZVpKXX7njuuSkvKDQCwBgt%2BY%3D; x-signature=GqtNLJQw2%2BOk1rO6sby9vQT3N5%2BiX8g%2B8Bqrndiv51dSYQFC4v2%2FlrkKmH1utjqExbfvVjtks0%2Bn6RC3q61%2FRA%3D%3D; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiMmM1OGEyMjQtMDViZi00NjllLWFkYjUtNmY3ODdmZDU1MTc2In0.rnUG2UvKv32YBGSyYWrWh_1KaLiTix-6H4TZe4ZywfU; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiMmM1OGEyMjQtMDViZi00NjllLWFkYjUtNmY3ODdmZDU1MTc2In0.rnUG2UvKv32YBGSyYWrWh_1KaLiTix-6H4TZe4ZywfU; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%22c02bd50a-db0e-4f35-903e-b03f32f28b5f%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D' \
-H 'Upgrade-Insecure-Requests: 1' \
-H 'Sec-Fetch-Dest: document' \
-H 'Sec-Fetch-Mode: navigate' \
-H 'Sec-Fetch-Site: same-site' \
-H 'Sec-Fetch-User: ?1' \
-H 'Priority: u=0, i'
@@ -0,0 +1,123 @@
use crate::client::browser_user_agents::FIREFOX_143_MAC_USER_AGENT;
use crate::datatypes::api::file_id::FileId;
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::utils::user_and_file_id_to_video_url::user_and_file_id_to_video_url;
use log::{error, info, warn};
use std::time::Duration;
use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, ORIGIN, REFERER, TE, UPGRADE_INSECURE_REQUESTS, USER_AGENT};
use wreq::Client;
use wreq_util::Emulation;
pub struct DownloadVideoFileArgs<'a> {
pub cookies: &'a str,
pub user_id: &'a UserId,
pub file_id: &'a FileId,
pub request_timeout: Option<Duration>,
}
pub struct VideoFile {
pub bytes: Vec<u8>,
}
pub async fn download_video_file(args: DownloadVideoFileArgs<'_>) -> Result<VideoFile, GrokError> {
let client = Client::builder()
.emulation(Emulation::Firefox143)
.build()
.map_err(|err| GrokClientError::WreqClientError(err))?;
let video_url = user_and_file_id_to_video_url(&args.user_id, &args.file_id, false);
info!("Video file URL: {}", video_url);
let mut request_builder = client.get(&video_url)
.header(USER_AGENT, FIREFOX_143_MAC_USER_AGENT)
.header(ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
.header(ACCEPT_ENCODING, "gzip, deflate, br, zstd")
.header(REFERER, "https://grok.com/")
.header("Sec-GPC", "1")
.header(CONNECTION, "keep-alive")
.header(COOKIE, args.cookies)
.header(UPGRADE_INSECURE_REQUESTS, "1")
.header("sec-fetch-dest", "document")
.header("sec-fetch-mode", "navigate")
.header("sec-fetch-site", "same-site")
.header("sec-fetch-user", "?1")
.header("priority", "u=0, i");
if let Some(timeout) = args.request_timeout {
request_builder = request_builder.timeout(timeout);
}
let response = request_builder.send()
.await
.map_err(|err| GrokGenericApiError::WreqError(err))?;
let status = response.status();
info!("Download video status: {:?}", status);
let download_bytes = &response.bytes()
.await
.map_err(|err| {
error!("Error reading Grok create media response body: {:?}", err);
GrokGenericApiError::WreqError(err)
})?;
// TODO: Classify errors
if !status.is_success() {
warn!("Not successful liking media (status: {:?})", status);
// 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));
}
Ok(VideoFile {
bytes: download_bytes.to_vec(),
})
}
#[cfg(test)]
mod tests {
use crate::datatypes::api::file_id::FileId;
use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
use crate::requests::download_file::download_video_file::{download_video_file, DownloadVideoFileArgs};
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
use errors::AnyhowResult;
#[tokio::test]
#[ignore] // Client side tests only
async fn test_download() -> AnyhowResult<()> {
//setup_test_logging(LevelFilter::Trace);
let cookies = get_typed_test_cookies()?;
let file_id = FileId("d9b300c6-9562-4e24-a87a-3ede2a53f0bc".to_string()); // Ernest
let secrets = request_client_secrets(RequestClientSecretsArgs {
cookies: &cookies,
}).await?;
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);
let download = download_video_file(DownloadVideoFileArgs {
cookies: cookies.as_str(),
user_id: &secrets.user_id,
file_id: &file_id,
request_timeout: None,
}).await?;
println!("Video byte length: {:?}", download.bytes.len());
assert_eq!(1, 2);
Ok(())
}
}
@@ -0,0 +1 @@
mod download_video_file;
@@ -0,0 +1,135 @@
curl 'wss://grok.com/ws/imagine/listen' \
-H 'Upgrade: websocket' \
-H 'Origin: https://grok.com' \
-H 'Cache-Control: no-cache' \
-H 'Accept-Language: en-US,en;q=0.9' \
-H 'Pragma: no-cache' \
-b 'cf_clearance=o10zUU.x20fFOkj6tPIzI73QFT.kG3rLwdtt5P9KNzI-1760710012-1.2.1.1-q0vgGudZ42zQLs1EEEdVDkqeJGaJxkL0m60w05izUQ4NVtFNawTmElVUNYS5gOXSRnCjQkTmcMu0bI4yw3gkf.0EyfR9fE3McXK9zjwnMDZLC5MDsYazzaMA87sU4seMjd3G8oKfdi.r2pZ2rUzkoLSAJ11.q7IpHUvWsk8zcpxm1hLg9LcvfX0c.Sbcf.8mwY_32NVpIZT_0rzdp71FJUOZGabSOK4isjC409u7GOg; _ga=GA1.1.1232202746.1760710013; i18nextLng=en; x-anonuserid=621371fd-a877-4245-900c-1aa8db6039f2; x-challenge=2CHUrpA5J8%2FW2%2F1H8glEE3WKIXTKrbU7mMARWLT5DHzp5MS6nUk1aTHt7gqzv1vCxfnIdHTdeq4ZLYwcBj1DL%2FBptSSVT9OKi9DQDsN%2BKk0Ur3jg1uS%2BuPtwYSNZ%2B6CtuLCrDZqlDh%2FfFTzYr2sQ9nT3R72HfOyS%2FFXw0qvCgoROCtZCiGY%3D; x-signature=qPh93ojC8uCXOUvq7t0SzcwsyXKOE%2BRXH6dH26oL8lxwRIabYha6eniSt329QvpjBEeUqlekeStaek44mDpEYg%3D%3D; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1760710013$o1$g1$t1760710085$j60$l0$h0' \
-H 'Connection: Upgrade' \
-H 'Sec-WebSocket-Key: 3cx4uqFUq7dJfGvZsbsVEg==' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits'
--------------------------------------
https://stackoverflow.com/questions/47860689/how-to-read-a-websocket-response-with-curl
curl --include \
--no-buffer \
-b 'cf_clearance=o10zUU.x20fFOkj6tPIzI73QFT.kG3rLwdtt5P9KNzI-1760710012-1.2.1.1-q0vgGudZ42zQLs1EEEdVDkqeJGaJxkL0m60w05izUQ4NVtFNawTmElVUNYS5gOXSRnCjQkTmcMu0bI4yw3gkf.0EyfR9fE3McXK9zjwnMDZLC5MDsYazzaMA87sU4seMjd3G8oKfdi.r2pZ2rUzkoLSAJ11.q7IpHUvWsk8zcpxm1hLg9LcvfX0c.Sbcf.8mwY_32NVpIZT_0rzdp71FJUOZGabSOK4isjC409u7GOg; _ga=GA1.1.1232202746.1760710013; i18nextLng=en; x-anonuserid=621371fd-a877-4245-900c-1aa8db6039f2; x-challenge=2CHUrpA5J8%2FW2%2F1H8glEE3WKIXTKrbU7mMARWLT5DHzp5MS6nUk1aTHt7gqzv1vCxfnIdHTdeq4ZLYwcBj1DL%2FBptSSVT9OKi9DQDsN%2BKk0Ur3jg1uS%2BuPtwYSNZ%2B6CtuLCrDZqlDh%2FfFTzYr2sQ9nT3R72HfOyS%2FFXw0qvCgoROCtZCiGY%3D; x-signature=qPh93ojC8uCXOUvq7t0SzcwsyXKOE%2BRXH6dH26oL8lxwRIabYha6eniSt329QvpjBEeUqlekeStaek44mDpEYg%3D%3D; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1760710013$o1$g1$t1760710085$j60$l0$h0' \
--header "Connection: Upgrade" \
--header "Upgrade: websocket" \
--header "Host: grok.com" \
--header "Origin: https://grok.com" \
--header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
--header "Sec-WebSocket-Version: 13" \
"https://grok.com/ws/imagine/listen"
-------------------------------------------
This turns a 401 into a 400 - it's a mix of:
- the above SO answer
- "copy as curl" from Chrome"
- curl-impersonate:
curl-impersonate-chrome -vvv 'wss://grok.com/ws/imagine/listen' \
--include \
--no-buffer \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'Host: grok.com' \
-H 'Origin: https://grok.com' \
-H 'Sec-WebSocket-Key: XjPVg9OQe7CJOH6U7lq5vw==' \
-H 'Sec-WebSocket-Version: 13' \
-b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=e7.LXFbmc.U81rZIqxuBfBe88yuBZMXEP.zwxNkbOxw-1760724394-1.2.1.1-JMHrapBGxcZriUw852.NCqwFRpZJFvoYxq.mV0jDpKCkiHBZwpdv09XJi6eFLJUYJJ6UDZ3c1eAsucVYLdWN.SvA9M6qzujj8nY4ym03PxQlMEd2OmXJogtDDJbPhA5AZEGxA39_6QQlvaBIUBnYPALYrTl9XJN_V4q3n4BXtpoBzrIJURMIn0mW3esUCGv0NukuVQrkrNtMgqT5SUmTGi0idaBYoR_2_wv4P09lsug; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
> Sec-WebSocket-Version: 13
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/1.1 400 Bad Request
< Date: Fri, 17 Oct 2025 18:15:29 GMT
< Content-Length: 0
< Connection: keep-alive
< speculation-rules: "/cdn-cgi/speculation"
< Server: cloudflare
< CF-RAY: 9901c814add7452d-ATL
* Refused WebSockets upgrade: 400
* Closing connection 0
* TLSv1.3 (OUT), TLS alert, close notify (256):
curl: (22) Refused WebSockets upgrade: 400
Removing the cookie turns it into a Cloudflare 401.
--------------------------------------------
--include --no-buffer --header "Connection: Upgrade" \
--header "Upgrade: websocket" \
--header "Host: example.com" \
--header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
--header "Sec-WebSocket-Version: 13" \
---------------------------------------------
Curl script works. We needed to remove headers that curl was automatically adding (and thus we were duplicating) :
curl-impersonate-chrome -vvv 'wss://grok.com/ws/imagine/listen' \
--include \
--no-buffer \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=e7.LXFbmc.U81rZIqxuBfBe88yuBZMXEP.zwxNkbOxw-1760724394-1.2.1.1-JMHrapBGxcZriUw852.NCqwFRpZJFvoYxq.mV0jDpKCkiHBZwpdv09XJi6eFLJUYJJ6UDZ3c1eAsucVYLdWN.SvA9M6qzujj8nY4ym03PxQlMEd2OmXJogtDDJbPhA5AZEGxA39_6QQlvaBIUBnYPALYrTl9XJN_V4q3n4BXtpoBzrIJURMIn0mW3esUCGv0NukuVQrkrNtMgqT5SUmTGi0idaBYoR_2_wv4P09lsug; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
> GET /ws/imagine/listen HTTP/1.1
> Host: grok.com
> Accept: */*
> Cookie: _ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=e7.LXFbmc.U81rZIqxuBfBe88yuBZMXEP.zwxNkbOxw-1760724394-1.2.1.1-JMHrapBGxcZriUw852.NCqwFRpZJFvoYxq.mV0jDpKCkiHBZwpdv09XJi6eFLJUYJJ6UDZ3c1eAsucVYLdWN.SvA9M6qzujj8nY4ym03PxQlMEd2OmXJogtDDJbPhA5AZEGxA39_6QQlvaBIUBnYPALYrTl9XJN_V4q3n4BXtpoBzrIJURMIn0mW3esUCGv0NukuVQrkrNtMgqT5SUmTGi0idaBYoR_2_wv4P09lsug; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0
> Connection: Upgrade
> Sec-WebSocket-Version: 13
> Sec-WebSocket-Key: JpMsHjXLPEmOsa89FQBeHA==
> Connection: Upgrade
> Upgrade: websocket
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/1.1 101 Switching Protocols
< Date: Fri, 17 Oct 2025 18:56:23 GMT
< Connection: upgrade
< Upgrade: websocket
< Sec-WebSocket-Accept: 67ERWy4vrrAcKi3+zna7zswEyOA=
< CF-Ray: 990203fe38bd78ce-ATL
< CF-Cache-Status: DYNAMIC
< Strict-Transport-Security: max-age=31536000; includeSubDomains
< x-trace-id: 638d9aa7a99ee6510e0fc833caecd273
< speculation-rules: "/cdn-cgi/speculation"
< Server: cloudflare
* Received 101, switch to WebSocket; mask af11ea2e
<
------------------------------------------------------------
Here are the internal headers from Wreq:
Execute... print headers...
WS Request Header: host: "grok.com"
WS Request Header: cookie: "_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=e7.LXFbmc.U81rZIqxuBfBe88yuBZMXEP.zwxNkbOxw-1760724394-1.2.1.1-JMHrapBGxcZriUw852.NCqwFRpZJFvoYxq.mV0jDpKCkiHBZwpdv09XJi6eFLJUYJJ6UDZ3c1eAsucVYLdWN.SvA9M6qzujj8nY4ym03PxQlMEd2OmXJogtDDJbPhA5AZEGxA39_6QQlvaBIUBnYPALYrTl9XJN_V4q3n4BXtpoBzrIJURMIn0mW3esUCGv0NukuVQrkrNtMgqT5SUmTGi0idaBYoR_2_wv4P09lsug; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0"
WS Request Header: connection: "upgrade"
WS Request Header: upgrade: "websocket"
WS Request Header: sec-websocket-version: "13"
WS Request Header: sec-websocket-key: "Dh44njJrey+qIso3X1IdhA=="
WS Request Header: user-agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
Response version: HTTP/1.1
@@ -0,0 +1,64 @@
Here are some potential ways we might be caught up in a WAF rule.
The other alternative is that it's some heuristic, potentially related to the handshake.
Dropdown
-----------
cookie - string matching tools
Cookie value of [name] [operator - string matching tools] [value]
Referer - string matching tools
Request method - equals / does not equal / in - strict list of [GET, POST, PURGE, PUT, HEAD, OPTIONS, DELETE, PATCH]
Hostname - string matching tools
SSL/HTTPS - checkbox
HTTP Version - equals / does not equal / in - strict list of [HTTP/1.0, HTTP/1.1, HTTP/1.2, HTTP/2, HTTP/3, SPDY/3.1]
User agent - string matching tools
Client certificate verified - checkbox
Header - map for any header name + string matching tools
Fallthrough detected - checkbox
X-Forwarded-For - string matching tools
URI, Path, Query, etc.
WAF Attack Score Class - Attack, Likely Attack, Likely Clean, Clean
Disposable email check - bool
User and password leaked - bool
Password Leaked - bool
Expressions
--------------
cookie:
(http.cookie wildcard r"asdf=foo*")
Cookie value of:
(any(http.request.cookies[""][*] matches ".*"))
Referer:
(http.referer eq "https://example.com")
Hostname:
(http.host eq "foo.com")
SSL/HTTPS:
(ssl) - strictly on
(not ssl) - strictly off
HTTP Version:
(http.request.version eq "HTTP/1.0")
User Agent:
(http.user_agent wildcard r"Mozilla/*")
Client Certificate Verified:
(cf.tls_client_auth.cert_verified) - check on
Header:
(any(http.request.headers["header-name"][*] eq "foo"))
Fallthrough detected:
(cf.api_gateway.fallthrough_detected)
X-Forwarded-For:
(http.x_forwarded_for wildcard r"192.0.*")
@@ -0,0 +1,43 @@
Chrome Raw Request Headers
==========================
GET wss://grok.com/ws/imagine/listen HTTP/1.1
Host: grok.com
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
Upgrade: websocket
Origin: https://grok.com
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Cookie: cf_clearance=o10zUU.x20fFOkj6tPIzI73QFT.kG3rLwdtt5P9KNzI-1760710012-1.2.1.1-q0vgGudZ42zQLs1EEEdVDkqeJGaJxkL0m60w05izUQ4NVtFNawTmElVUNYS5gOXSRnCjQkTmcMu0bI4yw3gkf.0EyfR9fE3McXK9zjwnMDZLC5MDsYazzaMA87sU4seMjd3G8oKfdi.r2pZ2rUzkoLSAJ11.q7IpHUvWsk8zcpxm1hLg9LcvfX0c.Sbcf.8mwY_32NVpIZT_0rzdp71FJUOZGabSOK4isjC409u7GOg; _ga=GA1.1.1232202746.1760710013; i18nextLng=en; x-anonuserid=621371fd-a877-4245-900c-1aa8db6039f2; x-challenge=2CHUrpA5J8%2FW2%2F1H8glEE3WKIXTKrbU7mMARWLT5DHzp5MS6nUk1aTHt7gqzv1vCxfnIdHTdeq4ZLYwcBj1DL%2FBptSSVT9OKi9DQDsN%2BKk0Ur3jg1uS%2BuPtwYSNZ%2B6CtuLCrDZqlDh%2FfFTzYr2sQ9nT3R72HfOyS%2FFXw0qvCgoROCtZCiGY%3D; x-signature=qPh93ojC8uCXOUvq7t0SzcwsyXKOE%2BRXH6dH26oL8lxwRIabYha6eniSt329QvpjBEeUqlekeStaek44mDpEYg%3D%3D; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1760710013$o1$g1$t1760710085$j60$l0$h0
Sec-WebSocket-Key: 3cx4uqFUq7dJfGvZsbsVEg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Firefox Raw Request Headers
===========================
GET /ws/imagine/listen HTTP/1.1
Host: grok.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Sec-WebSocket-Version: 13
Origin: https://grok.com
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: v901qtQaweRjQflydUViHw==
Sec-GPC: 1
Connection: keep-alive, Upgrade
Cookie: stblid=dea257e3-e71c-4214-88b8-11b0cac7152e; cf_clearance=4.JXCkTU2iJUEoDH1QieG_4ZyUsFLDHUdbAniQePfbk-1760685075-1.2.1.1-sPbWPHzZDlOVbMNiUrLrH9pmBI6te.Zwvy2IBz08XRe4FN8n7RaMsM6tnFJxV7bU4EhC53RPF4zvaGJTJeNicR.ETBJa9IWTEvnIYNLofI778oq.hNEn7mTi4Jh8x.vnQvdxdhmbzCase_MwGVMbDLe3YiHuP5NhCEvML1s61yt_0l00XTmo89le_7UGvLd2xUYaE2iYwSGwakyrJvNUp2Bs7GlLEbe2Agugi4TH_7w; i18nextLng=en; x-anonuserid=636cd954-e8e0-4592-aa72-9c8337b0b481; x-challenge=GZHDBobMfJjwZAjHkPRIUjkptE4EhMI%2FtZQCIUlbQ24JpOZNB80jAFPUVuqH1V33VQqf%2FI8E%2Bf0zvIgnkshHQbqMbRz1cidQa4fhmjCHVceIGQ%2BBFXME6z2JL6e%2FNJUFfa1gdzy6OC%2Bn%2FPVFY06KuZ5O445m2u5JTNnwJ8HyqRnNXUOhncA%3D; x-signature=ALNyXb5BnhvGpXzo2lSb64bZEjWrJRwDpWQYxacDa5wRp34HDP6tPsWkEk2Oe2DZSQtzkZSAwZmihrSf%2BnO9Vw%3D%3D; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiNjllNjhlMzQtM2RhNS00NTg0LTljMWMtODQ2OGYwZDMzMzM4In0.9DoSVTs4BEfPbO1oX72qMoRcM8vVUZ9Y461T2i5l_DQ; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiNjllNjhlMzQtM2RhNS00NTg0LTljMWMtODQ2OGYwZDMzMzM4In0.9DoSVTs4BEfPbO1oX72qMoRcM8vVUZ9Y461T2i5l_DQ; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%22b231bb76-612b-402a-896b-8d53fe0654e1%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: websocket
Sec-Fetch-Site: same-origin
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
@@ -0,0 +1,37 @@
Server Connection:
Address grok.com:443
Resolved Address 104.18.28.234:443
HTTP Version HTTP/1.1
ALPN http/1.1
Server Certificate:
Type EC (secp256r1), 256 bits
SHA256 digest bb 6e e7 1f 26 4c ea 10 5c a4 26 c1 93 3d f3 99 c5 a3 c9 e8 a6 e3 d3 03 e4 44 66 44 65 c1 3f 47
Valid from 2025-09-25 19:14:19+00:00
Valid to 2025-12-24 20:14:16+00:00
Serial 291992553975709925923951507305721322045
Subject CN grok.com
Issuer C US
O Google Trust Services
CN WE1
Alt names grok.com, typeahead.grok.com
Client Connection:
Address 127.0.0.1:63171
HTTP Version HTTP/1.1
TLS Version TLSv1.3
Server Name Indication grok.com
Cipher Name TLS_AES_256_GCM_SHA384
ALPN http/1.1
Timing:
Client conn. established 2025-10-18 01:02:06.884
Server conn. initiated 2025-10-18 01:02:06.888
Server conn. TCP handshake 2025-10-18 01:02:06.899
Server conn. TLS handshake 2025-10-18 01:02:06.923
Client conn. TLS handshake 2025-10-18 01:02:06.925
First request byte 2025-10-18 01:02:06.925
Request complete 2025-10-18 01:02:06.926
First response byte 2025-10-18 01:02:07.256
Response complete 2025-10-18 01:02:07.263
Client conn. closed –
Server conn. closed –
TODO: Now try this with wreq and see what the differences are.
@@ -0,0 +1,22 @@
Host: grok.com
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
Upgrade: websocket
Origin: https://grok.com
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Cookie: _ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTct
OWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzct
OTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; cf_clearanc
e=pHyVLkkYV0wHKuAHOqhI3paeuJoM5VqHFXksRO.5poo-1760763724-1.2.1.1-uNM2i.EbSArwG0hMIz04Y3yMvxMnHMeuXgGbK_qdNGRB1ElLRJhO8pO5xs9zd1ZiR9NYLQmk5Z8Z
BZazkIbsd0bHowJ06XjzcvWTDREB_enubIV82nhnkH_wVm2Cb9lQisSp0nAEeG7UubY4202XcmS3cDAiK_ihEXOI1zCiZdMC0JDgVThOU6HWunJrAiVkAQ880dhb.MmiJ2Lrs.8T1UdJQ
WmE.DXmWja3dyUxKmU; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24de
vice_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%
22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mp
r%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D;
_ga_8FEWB057YH=GS2.1.s1760763724$o4$g1$t1760763725$j59$l0$h0
Sec-WebSocket-Key: RB4Awj8U5qcXxoQfKQPVtQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
@@ -0,0 +1,11 @@
Date: Sat, 18 Oct 2025 05:02:07 GMT
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: gFxhyC2TQkeKiqvLNdJNu4OSeRI=
CF-Ray: 99057b4dc9661395-ATL
CF-Cache-Status: DYNAMIC
Strict-Transport-Security: max-age=31536000; includeSubDomains
x-trace-id: ba406937d0e501aa51e570cabc701502
Vary: Accept-Encoding
speculation-rules: "/cdn-cgi/speculation"
Server: cloudflare
@@ -0,0 +1,35 @@
Server Connection:
Address grok.com:443
Resolved Address 104.18.29.234:443
HTTP Version HTTP/1.1
ALPN http/1.1
Server Certificate:
Type EC (secp256r1), 256 bits
SHA256 digest bb 6e e7 1f 26 4c ea 10 5c a4 26 c1 93 3d f3 99 c5 a3 c9 e8 a6 e3 d3 03 e4 44 66 44 65 c1 3f 47
Valid from 2025-09-25 19:14:19+00:00
Valid to 2025-12-24 20:14:16+00:00
Serial 291992553975709925923951507305721322045
Subject CN grok.com
Issuer C US
O Google Trust Services
CN WE1
Alt names grok.com, typeahead.grok.com
Client Connection:
Address 127.0.0.1:62291
HTTP Version HTTP/1.1
TLS Version TLSv1.3
Server Name Indication grok.com
Cipher Name TLS_AES_256_GCM_SHA384
ALPN http/1.1
Timing:
Client conn. established 2025-10-18 00:58:39.371
Server conn. initiated 2025-10-18 00:58:39.372
Server conn. TCP handshake 2025-10-18 00:58:39.387
Server conn. TLS handshake 2025-10-18 00:58:39.423
Client conn. TLS handshake 2025-10-18 00:58:39.426
First request byte 2025-10-18 00:58:39.427
Request complete 2025-10-18 00:58:39.428
First response byte 2025-10-18 00:58:39.789
Response complete 2025-10-18 00:58:39.793
Client conn. closed –
Server conn. closed –
@@ -0,0 +1,27 @@
Host: grok.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Sec-WebSocket-Version: 13
Origin: https://grok.com
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: X2NHDjwqbk4quToBT5L97Q==
Connection: keep-alive, Upgrade
Cookie: stblid=4c0f7d0f-9768-49cf-94f7-1f59f1adf1f5; x-anon-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNmFhNTFmZmYtMmFlNC00NjU3LTk0ZD
ItNDFjMWQyMjNlZTUyIiwidGVhbV9pZCI6bnVsbCwiZXhwIjoxNzYwMDQyNDA0LCJpc19hbm9uX3VzZXIiOnRydWV9.ySBr18x0Tht-a3RE-cdtQDbWoyJ1SbB_JrHAnNSFzOM; cf_cl
earance=qYvZ0CfScz.kWrsB14nlWVFylzzOQA0LTnnMZhlCk8M-1760763505-1.2.1.1-RyM97EWXnZ7vo8iyqmM4pyrVP1OFSPC88v61zhD_hAkFtS37A96cvUNXlvaCiGVk5wjTtS
kpXcf9wq8wF7vLahSYBoHZXfCxfgeo8rN9BNDwaMXy1Czvj6rNu.OomL9gqoIUCTB7rixqDvJ0Bf9D5cGY5SQth8bLf0jP3rFwmR5oFQMZEdxiOj3zPWxb49EzRnZp_XXCXpiMSeUCABK
bZNHGFRlTGqgsf3L.VLb6fCs; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiM2Y3NDgzMTYtOTkyMC00ZTkzLTk1NzQtOTA3ZGN
kYzU4M2M5In0.9ne0Ab4cJiNMmntjQwxIfCrVcY6qmuSYMiNRSph1Y60; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiM2Y3NDgzMTYtOTkyMC0
0ZTkzLTk1NzQtOTA3ZGNkYzU4M2M5In0.9ne0Ab4cJiNMmntjQwxIfCrVcY6qmuSYMiNRSph1Y60; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%
22%3A%222613c794-fce7-4ade-9b22-6118f977ede8%22%2C%22%24device_id%22%3A%22c3c89bdd-b1ba-4939-9ab4-b03b4dfd1e60%22%2C%22%24initial_referrer%22
%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%
7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%222613c794-fce7-4ade-9b
22-6118f977ede8%22%2C%22%24search_engine%22%3A%22google%22%2C%22utm_source%22%3A%22x%22%2C%22utm_campaign%22%3A%22grok_imagine_post%22%7D
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: websocket
Sec-Fetch-Site: same-origin
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
@@ -0,0 +1,11 @@
Date: Sat, 18 Oct 2025 04:58:39 GMT
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: gxmm7igG63oWr4UtK/fEcddjHKM=
CF-Ray: 9905763d0a5bbfc9-ATL
CF-Cache-Status: DYNAMIC
Strict-Transport-Security: max-age=31536000; includeSubDomains
x-trace-id: 0c2ca537233b895a760b6b05af786558
Vary: Accept-Encoding
speculation-rules: "/cdn-cgi/speculation"
Server: cloudflare
@@ -0,0 +1,13 @@
Wreq request:
- Cookie sent before Host !! both chrome and firefox send `Host` first!
- `cookie` is lowercase
- `host` is lowercase
- `user-agent` sent twice!
- no origin header (firefox and chrome have it)
- sends "te: trailers" (chrome and firefox do not)
- sends `sec-fetch-dest: document` (firefox doesn't, crhome sends "empty")
- sends `sec-fetch-mode: navigate` (firefox doesn't, chrome sends "websocket)
- sends "priority"
@@ -0,0 +1,35 @@
Server Connection:
Address grok.com:443
Resolved Address 104.18.28.234:443
HTTP Version HTTP/1.1
ALPN http/1.1
Server Certificate:
Type EC (secp256r1), 256 bits
SHA256 digest bb 6e e7 1f 26 4c ea 10 5c a4 26 c1 93 3d f3 99 c5 a3 c9 e8 a6 e3 d3 03 e4 44 66 44 65 c1 3f 47
Valid from 2025-09-25 19:14:19+00:00
Valid to 2025-12-24 20:14:16+00:00
Serial 291992553975709925923951507305721322045
Subject CN grok.com
Issuer C US
O Google Trust Services
CN WE1
Alt names grok.com, typeahead.grok.com
Client Connection:
Address 127.0.0.1:62233
HTTP Version HTTP/1.1
TLS Version TLSv1.3
Server Name Indication grok.com
Cipher Name TLS_AES_256_GCM_SHA384
ALPN http/1.1
Timing:
Client conn. established 2025-10-18 21:56:28.925
Server conn. initiated 2025-10-18 21:56:28.930
Server conn. TCP handshake 2025-10-18 21:56:28.955
Server conn. TLS handshake 2025-10-18 21:56:28.980
Client conn. TLS handshake 2025-10-18 21:56:28.996
First request byte 2025-10-18 21:56:28.997
Request complete 2025-10-18 21:56:28.999
First response byte 2025-10-18 21:56:29.025
Response complete 2025-10-18 21:56:29.030
Server conn. closed 2025-10-18 21:56:29.033
Client conn. closed 2025-10-18 21:56:29.034
@@ -0,0 +1,23 @@
cookie: _ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWF
mMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC
00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b63
72b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519
ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__m
pso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24use
r_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0
host: grok.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0
Sec-WebSocket-Version: 13
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Key: ugZYrudLbtEpXGfdLOJY/A==
te: trailers
accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
accept-encoding: gzip, deflate, br, zstd
accept-language: en-US,en;q=0.5
sec-fetch-dest: document
sec-fetch-mode: navigate
sec-fetch-site: none
priority: u=0, i
Host: grok.com
@@ -0,0 +1,22 @@
host: grok.com
cookie: _ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWF
mMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC
00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b63
72b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519
ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__m
pso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24use
r_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0
Sec-WebSocket-Version: 13
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Key: j1hsQRD0iWa2FtuWAWsMDg==
te: trailers
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0
accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
accept-encoding: gzip, deflate, br, zstd
accept-language: en-US,en;q=0.5
sec-fetch-dest: document
sec-fetch-mode: navigate
sec-fetch-site: none
priority: u=0, i
Host: grok.com
@@ -0,0 +1,23 @@
Cookie: _ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTct
OWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzct
OTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da91
3ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4
913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A
%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A
%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0
origin: https://grok.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0
sec-websocket-extensions: permessage-deflate; client_max_window_bits
accept: */*
accept-language: en-US,en;q=0.9
sec-fetch-dest: empty
sec-fetch-mode: websocket
sec-fetch-site: same-origin
pragma: no-cache
cache-control: no-cache
Sec-WebSocket-Version: 13
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Key: aynq/tM4CYYkbbWoheTc2w==
accept-encoding: zstd,gzip,deflate,br
Host: grok.com
@@ -0,0 +1,27 @@
host: grok.com
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0
accept: */*
accept-language: en-US,en;q=0.5
accept-encoding: gzip, deflate, br, zstd
sec-websocket-version: 13
origin: https://grok.com
sec-websocket-extensions: permessage-deflate
sec-websocket-key: X2NHDjwqbk4quToBT5L97Q==
connection: keep-alive, Upgrade
cookie: stblid=4c0f7d0f-9768-49cf-94f7-1f59f1adf1f5; x-anon-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNmFhNTFmZmYtMmFlNC00NjU3LTk0ZD
ItNDFjMWQyMjNlZTUyIiwidGVhbV9pZCI6bnVsbCwiZXhwIjoxNzYwMDQyNDA0LCJpc19hbm9uX3VzZXIiOnRydWV9.ySBr18x0Tht-a3RE-cdtQDbWoyJ1SbB_JrHAnNSFzOM; cf_cl
earance=qYvZ0CfScz.kWrsB14nlWVFylzzOQA0LTnnMZhlCk8M-1760763505-1.2.1.1-RyM97EWXnZ7vo8iyqmM4pyrVP1OFSPC88v61zhD_hAkFtS37A96cvUNXlvaCiGVk5wjTtS
kpXcf9wq8wF7vLahSYBoHZXfCxfgeo8rN9BNDwaMXy1Czvj6rNu.OomL9gqoIUCTB7rixqDvJ0Bf9D5cGY5SQth8bLf0jP3rFwmR5oFQMZEdxiOj3zPWxb49EzRnZp_XXCXpiMSeUCABK
bZNHGFRlTGqgsf3L.VLb6fCs; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiM2Y3NDgzMTYtOTkyMC00ZTkzLTk1NzQtOTA3ZGN
kYzU4M2M5In0.9ne0Ab4cJiNMmntjQwxIfCrVcY6qmuSYMiNRSph1Y60; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiM2Y3NDgzMTYtOTkyMC0
0ZTkzLTk1NzQtOTA3ZGNkYzU4M2M5In0.9ne0Ab4cJiNMmntjQwxIfCrVcY6qmuSYMiNRSph1Y60; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%
22%3A%222613c794-fce7-4ade-9b22-6118f977ede8%22%2C%22%24device_id%22%3A%22c3c89bdd-b1ba-4939-9ab4-b03b4dfd1e60%22%2C%22%24initial_referrer%22
%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%
7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%222613c794-fce7-4ade-9b
22-6118f977ede8%22%2C%22%24search_engine%22%3A%22google%22%2C%22utm_source%22%3A%22x%22%2C%22utm_campaign%22%3A%22grok_imagine_post%22%7D
sec-fetch-dest: empty
sec-fetch-mode: websocket
sec-fetch-site: same-origin
pragma: no-cache
cache-control: no-cache
upgrade: websocket
@@ -0,0 +1,46 @@
Date: Sun, 19 Oct 2025 01:56:29 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
accept-ch: Sec-CH-UA-Bitness, Sec-CH-UA-Arch, Sec-CH-UA-Full-Version, Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform-Version,
Sec-CH-UA-Full-Version-List, Sec-CH-UA-Platform, Sec-CH-UA, UA-Bitness, UA-Arch, UA-Full-Version, UA-Mobile, UA-Model,
UA-Platform-Version, UA-Platform, UA
cf-mitigated: challenge
critical-ch: Sec-CH-UA-Bitness, Sec-CH-UA-Arch, Sec-CH-UA-Full-Version, Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform-Version,
Sec-CH-UA-Full-Version-List, Sec-CH-UA-Platform, Sec-CH-UA, UA-Bitness, UA-Arch, UA-Full-Version, UA-Mobile, UA-Model,
UA-Platform-Version, UA-Platform, UA
cross-origin-embedder-policy: require-corp
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: same-origin
origin-agent-cluster: ?1
permissions-policy: accelerometer=(),autoplay=(),browsing-topics=(),camera=(),clipboard-read=(),clipboard-write=(),geolocation=(),gyroscope=(),hid=(),interes
t-cohort=(),magnetometer=(),microphone=(),payment=(),publickey-credentials-get=(),screen-wake-lock=(),serial=(),sync-xhr=(),usb=()
referrer-policy: same-origin
server-timing: chlray;desc="990ca8c1baa0bd4b"
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
Cache-Control: private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Expires: Thu, 01 Jan 1970 00:00:01 GMT
Vary: Accept-Encoding
speculation-rules: "/cdn-cgi/speculation"
Server-Timing: cfOrigin;dur=0,cfEdge;dur=16
Server: cloudflare
CF-RAY: 990ca8c1baa0bd4b-ATL
Content-Encoding: gzip
XML/HTML [decoded gzip] [m:auto]
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Just a moment...</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="robots" content="noindex,nofollow">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
*{box-sizing:border-box;margin:0;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%;color:#313131;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe
UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color
Emoji"}body{display:flex;flex-direction:column;height:100vh;min-height:100vh}.main-content{margin:8rem auto;padding-left:1.5rem;max-width:60rem}@media (width
<= 720px){.main-content{margin-top:4rem}}.h2{line-height:2.25rem;font-size:1.5rem;font-weight:500}@media (width <= 720px){.h2{line-height:1.5rem;font-size:1.25rem}}#challenge-error-text{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iI0IyMEYwMyIgZD0iTTE2IDNhMTMgMTMgMCAxIDAgMTMgMTNBMTMuMDE1IDEzLjAxNSAwIDAgMCAxNiAzbTAgMjRhMTEgMTEgMCAxIDEgMTEtMTEgMTEuMDEgMTEuMDEgMCAwIDEtMTEgMTEiLz48cGF0aCBm
aWxsPSIjQjIwRjAzIiBkPSJNMTcuMDM4IDE4LjYxNUgxNC44N0wxNC41NjMgOS41aDIuNzgzem0tMS4wODQgMS40MjdxLjY2IDAgMS4wNTcuMzg4LjQwNy4zODkuNDA3Ljk5NCAwIC41OTYtLjQwNy45ODQtLjM5Ny4zOS0xLjA1Ny4zODktLjY1IDAtMS4wNTYtLjM4OS0uMzk4LS4zODktLjM5OC0uOTg0IDAtLjU5Ny4zOTgtLjk4NS40MDYtLjM5NyAxLjA1Ni0uMzk3Ii8+PC9zdmc+");background-repeat:no-repeat;background-size:conta
in;padding-left:34px}@media (prefers-color-scheme: dark){body{background-color:#222;color:#d9d9d9}}</style>
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
echo ""
echo "Websocket Test..."
echo ""
# NB: Without Cookie, it's a 403
# Without User-Agent, it's a 403
# IT WORKS !!!! -
# Curl was including `Sec-Websocket-Key` and `Sec-WebSocket-Version` automatically, so it GOT ADDED TWICE
# - It works without the `cf_clearance` cookie, too! (Under curl-impersonate-chrome)
# - It works with curl-impersonate-ff + Firefox 143 User-Agent (w/o cf_clearance cookie)
# - It works with curl-impersonate-ff + "weird user agent" (w/o cf_clearance cookie)
curl-impersonate-ff -vvv 'wss://grok.com/ws/imagine/listen' \
--include \
--no-buffer \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0'
#-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
#-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0'
#-b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=e7.LXFbmc.U81rZIqxuBfBe88yuBZMXEP.zwxNkbOxw-1760724394-1.2.1.1-JMHrapBGxcZriUw852.NCqwFRpZJFvoYxq.mV0jDpKCkiHBZwpdv09XJi6eFLJUYJJ6UDZ3c1eAsucVYLdWN.SvA9M6qzujj8nY4ym03PxQlMEd2OmXJogtDDJbPhA5AZEGxA39_6QQlvaBIUBnYPALYrTl9XJN_V4q3n4BXtpoBzrIJURMIn0mW3esUCGv0NukuVQrkrNtMgqT5SUmTGi0idaBYoR_2_wv4P09lsug; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0' \
#-H 'Origin: https://grok.com' \
#-H 'Host: grok.com' \
#-H 'Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==' \
#-H 'Sec-WebSocket-Version: 13' \
# -H 'Sec-WebSocket-Key: XjPVg9OQe7CJOH6U7lq5vw==' \
# -H 'Cache-Control: no-cache' \
# -H 'Accept-Language: en-US,en;q=0.9' \
# -H 'Pragma: no-cache' \
# -H 'Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits'
#curl-impersonate-ff --include \
#curl-impersonate-chrome --include \
@@ -0,0 +1,25 @@
# This was Rust -> mitm capture -> curl
# Cloudflare knows this is fake...
curl-impersonate-ff \
-vvv \
--http1.1 \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0' \
-H 'Accept: */*' \
-H 'Accept-Language: en-US,en;q=0.5' \
--compressed \
--include \
-H 'Sec-Websocket-Version: 13' \
-H 'Origin: https://grok.com' \
-H 'Sec-Websocket-Extensions: permessage-deflate' \
-H 'Sec-Websocket-Key: X2NHDjwqbk4quToBT5L97Q==' \
-H 'Cookie: stblid=4c0f7d0f-9768-49cf-94f7-1f59f1adf1f5; x-anon-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNmFhNTFmZmYtMmFlNC00NjU3LTk0ZDItNDFjMWQyMjNlZTUyIiwidGVhbV9pZCI6bnVsbCwiZXhwIjoxNzYwMDQyNDA0LCJpc19hbm9uX3VzZXIiOnRydWV9.ySBr18x0Tht-a3RE-cdtQDbWoyJ1SbB_JrHAnNSFzOM; cf_clearance=qYvZ0CfScz.kWrsB14nlWVFylzzOQA0LTnnMZhlCk8M-1760763505-1.2.1.1-RyM97EWXnZ7vo8iyqmM4pyrVP1OFSPC88v61zhD_hAkFtS37A96cvUNXlvaCiGVk5wjTtSkpXcf9wq8wF7vLahSYBoHZXfCxfgeo8rN9BNDwaMXy1Czvj6rNu.OomL9gqoIUCTB7rixqDvJ0Bf9D5cGY5SQth8bLf0jP3rFwmR5oFQMZEdxiOj3zPWxb49EzRnZp_XXCXpiMSeUCABKbZNHGFRlTGqgsf3L.VLb6fCs; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiM2Y3NDgzMTYtOTkyMC00ZTkzLTk1NzQtOTA3ZGNkYzU4M2M5In0.9ne0Ab4cJiNMmntjQwxIfCrVcY6qmuSYMiNRSph1Y60; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiM2Y3NDgzMTYtOTkyMC00ZTkzLTk1NzQtOTA3ZGNkYzU4M2M5In0.9ne0Ab4cJiNMmntjQwxIfCrVcY6qmuSYMiNRSph1Y60; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%222613c794-fce7-4ade-9b22-6118f977ede8%22%2C%22%24device_id%22%3A%22c3c89bdd-b1ba-4939-9ab4-b03b4dfd1e60%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%222613c794-fce7-4ade-9b22-6118f977ede8%22%2C%22%24search_engine%22%3A%22google%22%2C%22utm_source%22%3A%22x%22%2C%22utm_campaign%22%3A%22grok_imagine_post%22%7D' \
-H 'Sec-Fetch-Dest: empty' \
-H 'Sec-Fetch-Mode: websocket' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'Pragma: no-cache' \
-H 'Cache-Control: no-cache' \
https://grok.com/ws/imagine/listen
@@ -0,0 +1,14 @@
curl 'wss://grok.com/ws/imagine/listen' \
-X 'GET' \
-H 'Upgrade: websocket' \
-H 'Pragma: no-cache' \
-H 'Sec-WebSocket-Key: 03lrs8BvFhCjtaXxN8hRCQ==' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Extensions: permessage-deflate' \
-H 'Cache-Control: no-cache' \
-H 'Sec-Fetch-Mode: websocket' \
-H 'Origin: https://grok.com' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15' \
-H 'Connection: Upgrade' \
-H 'Sec-Fetch-Dest: websocket'
@@ -0,0 +1,81 @@
use crate::error::grok_client_error::GrokClientError;
use crate::error::grok_error::GrokError;
use std::time::Duration;
use wreq::header::{HeaderMap, USER_AGENT};
use wreq::{Client, Proxy};
use wreq_util::Emulation;
const FINGERPRINT_URL : &str = "https://tools.scrapfly.io/api/tls";
fn build_chrome_140_client() -> Result<Client, GrokError> {
Ok(Client::builder()
.emulation(Emulation::Chrome140)
.connection_verbose(true)
.connect_timeout(Duration::from_secs(10))
.build()
.map_err(|err| GrokClientError::WreqClientError(err))?)
}
fn build_firefox_139_client() -> Result<Client, GrokError> {
Ok(Client::builder()
.emulation(Emulation::Firefox139) // NB: THIS IS IDENTICAL IN FINGERPRINT TO REAL FIREFOX !!!
.connection_verbose(true)
.connect_timeout(Duration::from_secs(10))
.build()
.map_err(|err| GrokClientError::WreqClientError(err))?)
}
fn build_firefox_143_client() -> Result<Client, GrokError> {
Ok(Client::builder()
.emulation(Emulation::Firefox143) // NB: THIS IS IDENTICAL IN FINGERPRINT TO REAL FIREFOX !!!
.connection_verbose(true)
.connect_timeout(Duration::from_secs(10))
.build()
.map_err(|err| GrokClientError::WreqClientError(err))?)
}
fn build_raw_firefox_client_with_proxy() -> Result<Client, GrokError> {
let proxy = Proxy::https("http://127.0.0.1:8080")
.map_err(|err| GrokClientError::WreqClientError(err))?;
Ok(Client::builder()
.emulation(Emulation::Firefox143)
.default_headers(HeaderMap::new())
.proxy(proxy)
.cert_verification(false)
.connection_verbose(true)
.connect_timeout(Duration::from_secs(10))
.build()
.map_err(|err| GrokClientError::WreqClientError(err))?)
}
async fn get_with_client(client: &Client) -> Result<String, GrokError> {
let builder = client.get(FINGERPRINT_URL)
.header("Accept", "application/json");
let response = builder.send()
.await
.map_err(|err| GrokClientError::WreqClientError(err))?;
let body = response.text()
.await
.map_err(|err| GrokClientError::WreqClientError(err))?;
Ok(body)
}
#[cfg(test)]
mod tests {
use errors::AnyhowResult;
use super::*;
#[tokio::test]
#[ignore] // manually test
async fn test() -> AnyhowResult<()> {
let client = build_firefox_143_client()?;
let body = get_with_client(&client).await?;
println!("{}", body);
assert_eq!(1, 2);
Ok(())
}
}
@@ -0,0 +1,3 @@
Using this tool:
https://tools.scrapfly.io/api/tls
@@ -0,0 +1,86 @@
{
"ja3": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,27-51-43-65281-13-35-65037-5-10-11-17613-23-45-0-16-18,4588-29-23-24,0",
"ja3n": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-5-10-11-13-16-18-23-27-35-43-45-51-17613-65037-65281,4588-29-23-24,0",
"ja3_digest": "04967b9a0e5a7140df00afd80636bd0b",
"ja3n_digest": "dcefaf3f0e71d260d19dc1d0749c9278",
"scrapfly_fp": "version:772|ch_ciphers:GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|ch_extensions:GREASE-0-5-10-11-13-16-18-23-27-35-43-45-51-17613-65037-65281-GREASE|groups:GREASE-4588-29-23-24|points:0|compression:0|supported_versions:GREASE-772-771|supported_protocols:h2-http11|key_shares:GREASE-4588-29|psk:1|signature_algs:1027-2052-1025-1283-2053-1281-2054-1537|early_data:0|",
"scrapfly_fp_digest": "2084695c9595178c76ba04d5d080dab8",
"tls": {
"version": "0x0303 - TLS 1.2",
"ciphers": [
"0xCACA",
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA"
],
"curves": [
"TLS_GREASE (0xEAEA)",
"X25519MLKEM768 (4588)",
"X25519 (29)",
"secp256r1 (23)",
"secp384r1 (24)"
],
"extensions": [
"GREASE (0xFAFA)",
"compress_certificate (27) (IANA)",
"key_share (51) (IANA)",
"supported_versions (43) (IANA)",
"extensionRenegotiationInfo (boringssl) (65281) (IANA)",
"signature_algorithms (13) (IANA)",
"session_ticket (35) (IANA)",
"extensionEncryptedClientHello (65037) (boringssl)",
"status_request (5) (IANA)",
"supported_groups (10) (IANA)",
"ec_point_formats (11) (IANA)",
"extensionApplicationSettingsNewCodepoint (17613) (boringssl)",
"extended_master_secret (23) (IANA)",
"psk_key_exchange_modes (45) (IANA)",
"server_name (0) (IANA)",
"application_layer_protocol_negotiation (16) (IANA)",
"signed_certificate_timestamp (18) (IANA)",
"GREASE (0xEAEA)"
],
"points": [
"0x00"
],
"protocols": [
"h2",
"http/1.1"
],
"versions": [
"23130",
"772",
"771"
],
"handshake_duration": "82.787214ms",
"is_session_resumption": false,
"session_ticket_supported": true,
"support_secure_renegotiation": true,
"supported_tls_versions": [23130, 772, 771],
"supported_protocols": [
"h2",
"http11"
],
"signature_algorithms": [1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537],
"psk_key_exchange_mode": "AQ==",
"cert_compression_algorithms": "AA==",
"early_data": false,
"using_psk": false,
"selected_protocol": "h2",
"selected_curve_group": 4588,
"selected_cipher_suite": 4865,
"key_shares": [60138, 4588, 29]
}
}
@@ -0,0 +1,127 @@
{
"ja3": "772,4867-4866-4865-52393-52392-52394-49200-49196-49192-49188-49172-49162-159-107-57-65413-196-136-129-157-61-53-192-132-49199-49195-49191-49187-49171-49161-158-103-51-190-69-156-60-47-186-65-49169-49159-5-4-49170-49160-22-10-255,43-51-0-11-10-13-16,29-23-24-25,0",
"ja3n": "772,4867-4866-4865-52393-52392-52394-49200-49196-49192-49188-49172-49162-159-107-57-65413-196-136-129-157-61-53-192-132-49199-49195-49191-49187-49171-49161-158-103-51-190-69-156-60-47-186-65-49169-49159-5-4-49170-49160-22-10-255,0-10-11-13-16-43-51,29-23-24-25,0",
"ja3_digest": "24616f5fb8dca2ce982ecd4fc9151fae",
"ja3n_digest": "8e46727c37cb29291ec1c5b31cdbe987",
"scrapfly_fp": "version:772|ch_ciphers:4867-4866-4865-52393-52392-52394-49200-49196-49192-49188-49172-49162-159-107-57-65413-196-136-129-157-61-53-192-132-49199-49195-49191-49187-49171-49161-158-103-51-190-69-156-60-47-186-65-49169-49159-5-4-49170-49160-22-10-255|ch_extensions:0-10-11-13-16-43-51|groups:29-23-24-25|points:0|compression:0|supported_versions:772-771-770-769|supported_protocols:h2-http11|key_shares:29|psk:NO|signature_algs:2054-1537-1539-2053-1281-1283-2052-1025-1027-513-515|early_data:0|",
"scrapfly_fp_digest": "f0ccdcf334afd6553b6f4edcf4bf8d33",
"tls": {
"version": "0x0303 - TLS 1.2",
"ciphers": [
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
"TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
"0xFF85",
"0x00C4",
"TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA",
"TLS_GOSTR341001_WITH_28147_CNT_IMIT",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_256_CBC_SHA256",
"TLS_RSA_WITH_AES_256_CBC_SHA",
"0x00C0",
"TLS_RSA_WITH_CAMELLIA_256_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256",
"TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_128_CBC_SHA256",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256",
"TLS_RSA_WITH_CAMELLIA_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_RC4_128_SHA",
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
"TLS_RSA_WITH_RC4_128_SHA",
"TLS_RSA_WITH_RC4_128_MD5",
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
"TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_EMPTY_RENEGOTIATION_INFO"
],
"curves": [
"X25519 (29)",
"secp256r1 (23)",
"secp384r1 (24)",
"secp521r1 (25)"
],
"extensions": [
"supported_versions (43) (IANA)",
"key_share (51) (IANA)",
"server_name (0) (IANA)",
"ec_point_formats (11) (IANA)",
"supported_groups (10) (IANA)",
"signature_algorithms (13) (IANA)",
"application_layer_protocol_negotiation (16) (IANA)"
],
"points": [
"0x00"
],
"protocols": [
"h2",
"http/1.1"
],
"versions": [
"772",
"771",
"770",
"769"
],
"handshake_duration": "86.820522ms",
"is_session_resumption": false,
"session_ticket_supported": false,
"support_secure_renegotiation": true,
"supported_tls_versions": [
772,
771,
770,
769
],
"supported_protocols": [
"h2",
"http11"
],
"signature_algorithms": [
2054,
1537,
1539,
2053,
1281,
1283,
2052,
1025,
1027,
513,
515
],
"psk_key_exchange_mode": null,
"cert_compression_algorithms": "AA==",
"early_data": false,
"using_psk": false,
"selected_protocol": "h2",
"selected_curve_group": 29,
"selected_cipher_suite": 4867,
"key_shares": [
29
]
}
}
@@ -0,0 +1,110 @@
{
"ja3": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49161-49171-49162-49172-156-157-47-53-60-61-49187-49188-49192,0-23-65281-10-11-35-16-5-13-18-51-45-43,29-23-24,0",
"ja3n": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49161-49171-49162-49172-156-157-47-53-60-61-49187-49188-49192,0-5-10-11-13-16-18-23-35-43-45-51-65281,29-23-24,0",
"ja3_digest": "5850656aed030b27422e4cfe91832137",
"ja3n_digest": "606e16090d0334d2b95d58971cbd4a85",
"scrapfly_fp": "version:772|ch_ciphers:GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49161-49171-49162-49172-156-157-47-53-60-61-49187-49188-49192|ch_extensions:GREASE-0-5-10-11-13-16-18-23-35-43-45-51-65281-GREASE|groups:GREASE-29-23-24|points:0|compression:0|supported_versions:GREASE-772-771-770-769|supported_protocols:h2-http11|key_shares:GREASE-29|psk:1|signature_algs:1027-2052-1025-1283-2053-1281-2054-1537|early_data:0|",
"scrapfly_fp_digest": "bad746f6af606b897818e06b7299d9e1",
"tls": {
"version": "0x0303 - TLS 1.2",
"ciphers": [
"0x3A3A",
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_128_CBC_SHA256",
"TLS_RSA_WITH_AES_256_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384"
],
"curves": [
"TLS_GREASE (0x1A1A)",
"X25519 (29)",
"secp256r1 (23)",
"secp384r1 (24)"
],
"extensions": [
"GREASE (0xDADA)",
"server_name (0) (IANA)",
"extended_master_secret (23) (IANA)",
"extensionRenegotiationInfo (boringssl) (65281) (IANA)",
"supported_groups (10) (IANA)",
"ec_point_formats (11) (IANA)",
"session_ticket (35) (IANA)",
"application_layer_protocol_negotiation (16) (IANA)",
"status_request (5) (IANA)",
"signature_algorithms (13) (IANA)",
"signed_certificate_timestamp (18) (IANA)",
"key_share (51) (IANA)",
"psk_key_exchange_modes (45) (IANA)",
"supported_versions (43) (IANA)",
"GREASE (0x0A0A)",
"padding (21) (IANA)"
],
"points": [
"0x00"
],
"protocols": [
"h2",
"http/1.1"
],
"versions": [
"27242",
"772",
"771",
"770",
"769"
],
"handshake_duration": "81.996852ms",
"is_session_resumption": false,
"session_ticket_supported": true,
"support_secure_renegotiation": true,
"supported_tls_versions": [
27242,
772,
771,
770,
769
],
"supported_protocols": [
"h2",
"http11"
],
"signature_algorithms": [
1027,
2052,
1025,
1283,
2053,
1281,
2054,
1537
],
"psk_key_exchange_mode": "AQ==",
"cert_compression_algorithms": "AA==",
"early_data": false,
"using_psk": false,
"selected_protocol": "h2",
"selected_curve_group": 29,
"selected_cipher_suite": 4865,
"key_shares": [
6682,
29
]
}
}
@@ -0,0 +1,120 @@
{
"ja3": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49187-49191-49172-158-52394-159-51-50-103-57-56-107-22-19-156-157-47-60-53-61-10-5-4,0-23-65281-10-11-35-16-5-34-51-43-13-45-28,29-23-24-25-256-257,0",
"ja3n": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49187-49191-49172-158-52394-159-51-50-103-57-56-107-22-19-156-157-47-60-53-61-10-5-4,0-5-10-11-13-16-23-28-34-35-43-45-51-65281,29-23-24-25-256-257,0",
"ja3_digest": "fd4c25ca69d1fcaed40c6c9a02faa54e",
"ja3n_digest": "0351cd0b39726e68b27dc34b44a4fbad",
"scrapfly_fp": "version:772|ch_ciphers:4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49187-49191-49172-158-52394-159-51-50-103-57-56-107-22-19-156-157-47-60-53-61-10-5-4|ch_extensions:0-5-10-11-13-16-23-28-34-35-43-45-51-65281|groups:29-23-24-25-256-257|points:0|compression:0|supported_versions:772-771|supported_protocols:h2-http11|key_shares:29-23|psk:1|signature_algs:1027-1283-1539-2052-2053-2054-1025-1281-1537-515-513|early_data:0|",
"scrapfly_fp_digest": "f9599c49ad3166009db13c9168a3368d",
"tls": {
"version": "0x0303 - TLS 1.2",
"ciphers": [
"TLS_AES_128_GCM_SHA256",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
"TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_128_CBC_SHA256",
"TLS_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA256",
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_RSA_WITH_RC4_128_SHA",
"TLS_RSA_WITH_RC4_128_MD5"
],
"curves": [
"X25519 (29)",
"secp256r1 (23)",
"secp384r1 (24)",
"secp521r1 (25)",
"ffdhe2048 (256)",
"ffdhe3072 (257)"
],
"extensions": [
"server_name (0) (IANA)",
"extended_master_secret (23) (IANA)",
"extensionRenegotiationInfo (boringssl) (65281) (IANA)",
"supported_groups (10) (IANA)",
"ec_point_formats (11) (IANA)",
"session_ticket (35) (IANA)",
"application_layer_protocol_negotiation (16) (IANA)",
"status_request (5) (IANA)",
"delegated_credentials (34) (IANA)",
"key_share (51) (IANA)",
"supported_versions (43) (IANA)",
"signature_algorithms (13) (IANA)",
"psk_key_exchange_modes (45) (IANA)",
"record_size_limit (28) (IANA)",
"padding (21) (IANA)"
],
"points": [
"0x00"
],
"protocols": [
"h2",
"http/1.1"
],
"versions": [
"772",
"771"
],
"handshake_duration": "92.872472ms",
"is_session_resumption": false,
"session_ticket_supported": true,
"support_secure_renegotiation": true,
"supported_tls_versions": [
772,
771
],
"supported_protocols": [
"h2",
"http11"
],
"signature_algorithms": [
1027,
1283,
1539,
2052,
2053,
2054,
1025,
1281,
1537,
515,
513
],
"psk_key_exchange_mode": "AQ==",
"cert_compression_algorithms": "AA==",
"early_data": false,
"using_psk": false,
"selected_protocol": "h2",
"selected_curve_group": 29,
"selected_cipher_suite": 4865,
"key_shares": [
29,
23
]
}
}
@@ -0,0 +1,106 @@
{
"ja3": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-34-18-51-43-13-45-28-27-65037,4588-29-23-24-25-256-257,0",
"ja3n": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-5-10-11-13-16-18-23-27-28-34-35-43-45-51-65037-65281,4588-29-23-24-25-256-257,0",
"ja3_digest": "d87a30a5782a73a83c1544bb06332780",
"ja3n_digest": "28ecc2d2875b345cecbb632b12d8c1e0",
"scrapfly_fp": "version:772|ch_ciphers:4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53|ch_extensions:0-5-10-11-13-16-18-23-27-28-34-35-43-45-51-65037-65281|groups:4588-29-23-24-25-256-257|points:0|compression:0|supported_versions:772-771|supported_protocols:h2-http11|key_shares:4588-29-23|psk:1|signature_algs:1027-1283-1539-2052-2053-2054-1025-1281-1537-515-513|early_data:0|",
"scrapfly_fp_digest": "5e7810e397137d931ef22c881d5c0808",
"tls": {
"version": "0x0303 - TLS 1.2",
"ciphers": [
"TLS_AES_128_GCM_SHA256",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA"
],
"curves": [
"X25519MLKEM768 (4588)",
"X25519 (29)",
"secp256r1 (23)",
"secp384r1 (24)",
"secp521r1 (25)",
"ffdhe2048 (256)",
"ffdhe3072 (257)"
],
"extensions": [
"server_name (0) (IANA)",
"extended_master_secret (23) (IANA)",
"extensionRenegotiationInfo (boringssl) (65281) (IANA)",
"supported_groups (10) (IANA)",
"ec_point_formats (11) (IANA)",
"session_ticket (35) (IANA)",
"application_layer_protocol_negotiation (16) (IANA)",
"status_request (5) (IANA)",
"delegated_credentials (34) (IANA)",
"signed_certificate_timestamp (18) (IANA)",
"key_share (51) (IANA)",
"supported_versions (43) (IANA)",
"signature_algorithms (13) (IANA)",
"psk_key_exchange_modes (45) (IANA)",
"record_size_limit (28) (IANA)",
"compress_certificate (27) (IANA)",
"extensionEncryptedClientHello (65037) (boringssl)"
],
"points": [
"0x00"
],
"protocols": [
"h2",
"http/1.1"
],
"versions": [
"772",
"771"
],
"handshake_duration": "88.264134ms",
"is_session_resumption": false,
"session_ticket_supported": true,
"support_secure_renegotiation": true,
"supported_tls_versions": [
772,
771
],
"supported_protocols": [
"h2",
"http11"
],
"signature_algorithms": [
1027,
1283,
1539,
2052,
2053,
2054,
1025,
1281,
1537,
515,
513
],
"psk_key_exchange_mode": "AQ==",
"cert_compression_algorithms": "AA==",
"early_data": false,
"using_psk": false,
"selected_protocol": "h2",
"selected_curve_group": 4588,
"selected_cipher_suite": 4865,
"key_shares": [
4588,
29,
23
]
}
}
@@ -0,0 +1,104 @@
{
"ja3": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-16-5-34-18-51-43-13-28-27-65037,4588-29-23-24-25-256-257,0",
"ja3n": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-5-10-11-13-16-18-23-27-28-34-43-51-65037-65281,4588-29-23-24-25-256-257,0",
"ja3_digest": "553a54f75f3fbbcc43b047f33887ed37",
"ja3n_digest": "21e3c7607b9ec450b2e4177a9e5733c7",
"scrapfly_fp": "version:772|ch_ciphers:4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53|ch_extensions:0-5-10-11-13-16-18-23-27-28-34-43-51-65037-65281|groups:4588-29-23-24-25-256-257|points:0|compression:0|supported_versions:772-771|supported_protocols:h2-http11|key_shares:4588-29-23|psk:NO|signature_algs:1027-1283-1539-2052-2053-2054-1025-1281-1537-515-513|early_data:0|",
"scrapfly_fp_digest": "ec622487a23fa9313b8156a5fd224c20",
"tls": {
"version": "0x0303 - TLS 1.2",
"ciphers": [
"TLS_AES_128_GCM_SHA256",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA"
],
"curves": [
"X25519MLKEM768 (4588)",
"X25519 (29)",
"secp256r1 (23)",
"secp384r1 (24)",
"secp521r1 (25)",
"ffdhe2048 (256)",
"ffdhe3072 (257)"
],
"extensions": [
"server_name (0) (IANA)",
"extended_master_secret (23) (IANA)",
"extensionRenegotiationInfo (boringssl) (65281) (IANA)",
"supported_groups (10) (IANA)",
"ec_point_formats (11) (IANA)",
"application_layer_protocol_negotiation (16) (IANA)",
"status_request (5) (IANA)",
"delegated_credentials (34) (IANA)",
"signed_certificate_timestamp (18) (IANA)",
"key_share (51) (IANA)",
"supported_versions (43) (IANA)",
"signature_algorithms (13) (IANA)",
"record_size_limit (28) (IANA)",
"compress_certificate (27) (IANA)",
"extensionEncryptedClientHello (65037) (boringssl)"
],
"points": [
"0x00"
],
"protocols": [
"h2",
"http/1.1"
],
"versions": [
"772",
"771"
],
"handshake_duration": "94.557783ms",
"is_session_resumption": false,
"session_ticket_supported": false,
"support_secure_renegotiation": true,
"supported_tls_versions": [
772,
771
],
"supported_protocols": [
"h2",
"http11"
],
"signature_algorithms": [
1027,
1283,
1539,
2052,
2053,
2054,
1025,
1281,
1537,
515,
513
],
"psk_key_exchange_mode": null,
"cert_compression_algorithms": "AA==",
"early_data": false,
"using_psk": false,
"selected_protocol": "h2",
"selected_curve_group": 4588,
"selected_cipher_suite": 4865,
"key_shares": [
4588,
29,
23
]
}
}
@@ -0,0 +1,103 @@
{
"ja3": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,65037-23-13-11-17613-16-5-45-51-0-35-65281-18-27-43-10,4588-29-23-24,0",
"ja3n": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-5-10-11-13-16-18-23-27-35-43-45-51-17613-65037-65281,4588-29-23-24,0",
"ja3_digest": "d32a22eaeec9c58fb7db3d14f344aad0",
"ja3n_digest": "dcefaf3f0e71d260d19dc1d0749c9278",
"scrapfly_fp": "version:772|ch_ciphers:GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|ch_extensions:GREASE-0-5-10-11-13-16-18-23-27-35-43-45-51-17613-65037-65281-GREASE|groups:GREASE-4588-29-23-24|points:0|compression:0|supported_versions:GREASE-772-771|supported_protocols:h2-http11|key_shares:GREASE-4588-29|psk:1|signature_algs:1027-2052-1025-1283-2053-1281-2054-1537|early_data:0|",
"scrapfly_fp_digest": "2084695c9595178c76ba04d5d080dab8",
"tls": {
"version": "0x0303 - TLS 1.2",
"ciphers": [
"0x9A9A",
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA"
],
"curves": [
"TLS_GREASE (0xAAAA)",
"X25519MLKEM768 (4588)",
"X25519 (29)",
"secp256r1 (23)",
"secp384r1 (24)"
],
"extensions": [
"GREASE (0xCACA)",
"extensionEncryptedClientHello (65037) (boringssl)",
"extended_master_secret (23) (IANA)",
"signature_algorithms (13) (IANA)",
"ec_point_formats (11) (IANA)",
"extensionApplicationSettingsNewCodepoint (17613) (boringssl)",
"application_layer_protocol_negotiation (16) (IANA)",
"status_request (5) (IANA)",
"psk_key_exchange_modes (45) (IANA)",
"key_share (51) (IANA)",
"server_name (0) (IANA)",
"session_ticket (35) (IANA)",
"extensionRenegotiationInfo (boringssl) (65281) (IANA)",
"signed_certificate_timestamp (18) (IANA)",
"compress_certificate (27) (IANA)",
"supported_versions (43) (IANA)",
"supported_groups (10) (IANA)",
"GREASE (0xBABA)"
],
"points": [
"0x00"
],
"protocols": [
"h2",
"http/1.1"
],
"versions": [
"6682",
"772",
"771"
],
"handshake_duration": "80.965498ms",
"is_session_resumption": false,
"session_ticket_supported": true,
"support_secure_renegotiation": true,
"supported_tls_versions": [
6682,
772,
771
],
"supported_protocols": [
"h2",
"http11"
],
"signature_algorithms": [
1027,
2052,
1025,
1283,
2053,
1281,
2054,
1537
],
"psk_key_exchange_mode": "AQ==",
"cert_compression_algorithms": "AA==",
"early_data": false,
"using_psk": false,
"selected_protocol": "h2",
"selected_curve_group": 4588,
"selected_cipher_suite": 4865,
"key_shares": [
43690,
4588,
29
]
}
}
@@ -0,0 +1,106 @@
{
"ja3": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-34-18-51-43-13-45-28-27-65037,4588-29-23-24-25-256-257,0",
"ja3n": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-5-10-11-13-16-18-23-27-28-34-35-43-45-51-65037-65281,4588-29-23-24-25-256-257,0",
"ja3_digest": "d87a30a5782a73a83c1544bb06332780",
"ja3n_digest": "28ecc2d2875b345cecbb632b12d8c1e0",
"scrapfly_fp": "version:772|ch_ciphers:4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53|ch_extensions:0-5-10-11-13-16-18-23-27-28-34-35-43-45-51-65037-65281|groups:4588-29-23-24-25-256-257|points:0|compression:0|supported_versions:772-771|supported_protocols:h2-http11|key_shares:4588-29-23|psk:1|signature_algs:1027-1283-1539-2052-2053-2054-1025-1281-1537-515-513|early_data:0|",
"scrapfly_fp_digest": "5e7810e397137d931ef22c881d5c0808",
"tls": {
"version": "0x0303 - TLS 1.2",
"ciphers": [
"TLS_AES_128_GCM_SHA256",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA"
],
"curves": [
"X25519MLKEM768 (4588)",
"X25519 (29)",
"secp256r1 (23)",
"secp384r1 (24)",
"secp521r1 (25)",
"ffdhe2048 (256)",
"ffdhe3072 (257)"
],
"extensions": [
"server_name (0) (IANA)",
"extended_master_secret (23) (IANA)",
"extensionRenegotiationInfo (boringssl) (65281) (IANA)",
"supported_groups (10) (IANA)",
"ec_point_formats (11) (IANA)",
"session_ticket (35) (IANA)",
"application_layer_protocol_negotiation (16) (IANA)",
"status_request (5) (IANA)",
"delegated_credentials (34) (IANA)",
"signed_certificate_timestamp (18) (IANA)",
"key_share (51) (IANA)",
"supported_versions (43) (IANA)",
"signature_algorithms (13) (IANA)",
"psk_key_exchange_modes (45) (IANA)",
"record_size_limit (28) (IANA)",
"compress_certificate (27) (IANA)",
"extensionEncryptedClientHello (65037) (boringssl)"
],
"points": [
"0x00"
],
"protocols": [
"h2",
"http/1.1"
],
"versions": [
"772",
"771"
],
"handshake_duration": "83.097037ms",
"is_session_resumption": false,
"session_ticket_supported": true,
"support_secure_renegotiation": true,
"supported_tls_versions": [
772,
771
],
"supported_protocols": [
"h2",
"http11"
],
"signature_algorithms": [
1027,
1283,
1539,
2052,
2053,
2054,
1025,
1281,
1537,
515,
513
],
"psk_key_exchange_mode": "AQ==",
"cert_compression_algorithms": "AA==",
"early_data": false,
"using_psk": false,
"selected_protocol": "h2",
"selected_curve_group": 4588,
"selected_cipher_suite": 4865,
"key_shares": [
4588,
29,
23
]
}
}
@@ -0,0 +1,106 @@
{
"ja3": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-34-18-51-43-13-45-28-27-65037,4588-29-23-24-25-256-257,0",
"ja3n": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-5-10-11-13-16-18-23-27-28-34-35-43-45-51-65037-65281,4588-29-23-24-25-256-257,0",
"ja3_digest": "d87a30a5782a73a83c1544bb06332780",
"ja3n_digest": "28ecc2d2875b345cecbb632b12d8c1e0",
"scrapfly_fp": "version:772|ch_ciphers:4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53|ch_extensions:0-5-10-11-13-16-18-23-27-28-34-35-43-45-51-65037-65281|groups:4588-29-23-24-25-256-257|points:0|compression:0|supported_versions:772-771|supported_protocols:h2-http11|key_shares:4588-29-23|psk:1|signature_algs:1027-1283-1539-2052-2053-2054-1025-1281-1537-515-513|early_data:0|",
"scrapfly_fp_digest": "5e7810e397137d931ef22c881d5c0808",
"tls": {
"version": "0x0303 - TLS 1.2",
"ciphers": [
"TLS_AES_128_GCM_SHA256",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA"
],
"curves": [
"X25519MLKEM768 (4588)",
"X25519 (29)",
"secp256r1 (23)",
"secp384r1 (24)",
"secp521r1 (25)",
"ffdhe2048 (256)",
"ffdhe3072 (257)"
],
"extensions": [
"server_name (0) (IANA)",
"extended_master_secret (23) (IANA)",
"extensionRenegotiationInfo (boringssl) (65281) (IANA)",
"supported_groups (10) (IANA)",
"ec_point_formats (11) (IANA)",
"session_ticket (35) (IANA)",
"application_layer_protocol_negotiation (16) (IANA)",
"status_request (5) (IANA)",
"delegated_credentials (34) (IANA)",
"signed_certificate_timestamp (18) (IANA)",
"key_share (51) (IANA)",
"supported_versions (43) (IANA)",
"signature_algorithms (13) (IANA)",
"psk_key_exchange_modes (45) (IANA)",
"record_size_limit (28) (IANA)",
"compress_certificate (27) (IANA)",
"extensionEncryptedClientHello (65037) (boringssl)"
],
"points": [
"0x00"
],
"protocols": [
"h2",
"http/1.1"
],
"versions": [
"772",
"771"
],
"handshake_duration": "94.016888ms",
"is_session_resumption": false,
"session_ticket_supported": true,
"support_secure_renegotiation": true,
"supported_tls_versions": [
772,
771
],
"supported_protocols": [
"h2",
"http11"
],
"signature_algorithms": [
1027,
1283,
1539,
2052,
2053,
2054,
1025,
1281,
1537,
515,
513
],
"psk_key_exchange_mode": "AQ==",
"cert_compression_algorithms": "AA==",
"early_data": false,
"using_psk": false,
"selected_protocol": "h2",
"selected_curve_group": 4588,
"selected_cipher_suite": 4865,
"key_shares": [
4588,
29,
23
]
}
}
@@ -0,0 +1,106 @@
{
"ja3": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-34-18-51-43-13-45-28-27-65037,4588-29-23-24-25-256-257,0",
"ja3n": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-5-10-11-13-16-18-23-27-28-34-35-43-45-51-65037-65281,4588-29-23-24-25-256-257,0",
"ja3_digest": "d87a30a5782a73a83c1544bb06332780",
"ja3n_digest": "28ecc2d2875b345cecbb632b12d8c1e0",
"scrapfly_fp": "version:772|ch_ciphers:4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53|ch_extensions:0-5-10-11-13-16-18-23-27-28-34-35-43-45-51-65037-65281|groups:4588-29-23-24-25-256-257|points:0|compression:0|supported_versions:772-771|supported_protocols:h2-http11|key_shares:4588-29-23|psk:1|signature_algs:1027-1283-1539-2052-2053-2054-1025-1281-1537-515-513|early_data:0|",
"scrapfly_fp_digest": "5e7810e397137d931ef22c881d5c0808",
"tls": {
"version": "0x0303 - TLS 1.2",
"ciphers": [
"TLS_AES_128_GCM_SHA256",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA"
],
"curves": [
"X25519MLKEM768 (4588)",
"X25519 (29)",
"secp256r1 (23)",
"secp384r1 (24)",
"secp521r1 (25)",
"ffdhe2048 (256)",
"ffdhe3072 (257)"
],
"extensions": [
"server_name (0) (IANA)",
"extended_master_secret (23) (IANA)",
"extensionRenegotiationInfo (boringssl) (65281) (IANA)",
"supported_groups (10) (IANA)",
"ec_point_formats (11) (IANA)",
"session_ticket (35) (IANA)",
"application_layer_protocol_negotiation (16) (IANA)",
"status_request (5) (IANA)",
"delegated_credentials (34) (IANA)",
"signed_certificate_timestamp (18) (IANA)",
"key_share (51) (IANA)",
"supported_versions (43) (IANA)",
"signature_algorithms (13) (IANA)",
"psk_key_exchange_modes (45) (IANA)",
"record_size_limit (28) (IANA)",
"compress_certificate (27) (IANA)",
"extensionEncryptedClientHello (65037) (boringssl)"
],
"points": [
"0x00"
],
"protocols": [
"h2",
"http/1.1"
],
"versions": [
"772",
"771"
],
"handshake_duration": "79.730582ms",
"is_session_resumption": false,
"session_ticket_supported": true,
"support_secure_renegotiation": true,
"supported_tls_versions": [
772,
771
],
"supported_protocols": [
"h2",
"http11"
],
"signature_algorithms": [
1027,
1283,
1539,
2052,
2053,
2054,
1025,
1281,
1537,
515,
513
],
"psk_key_exchange_mode": "AQ==",
"cert_compression_algorithms": "AA==",
"early_data": false,
"using_psk": false,
"selected_protocol": "h2",
"selected_curve_group": 4588,
"selected_cipher_suite": 4865,
"key_shares": [
4588,
29,
23
]
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
Received message: {"type":"image","blob":"...","job_id":"260270db-2515-470b-a730-1fe87e11d21c","grid_index":0,"is_alteration":false,"order":0,"section_id":0,"request_id":"57b94c87-ae13-4ade-8fc0-ebd39fb511ec","url":"https://imagine-public.x.ai/imagine-public/images/260270db-2515-470b-a730-1fe87e11d21c.png","id":"260270db-2515-470b-a730-1fe87e11d21c","prompt":"a dog riding a motorcycle","full_prompt":"a dog riding a motorcycle","percentage_complete":null,"model_name":"imagine_x_1","suggested_moderation_rule":null,"r_rated":false}
Received message: {"type":"image","blob":"...","job_id":"260270db-2515-470b-a730-1fe87e11d21c","grid_index":0,"is_alteration":false,"order":0,"section_id":0,"request_id":"57b94c87-ae13-4ade-8fc0-ebd39fb511ec","url":"https://imagine-public.x.ai/imagine-public/images/260270db-2515-470b-a730-1fe87e11d21c.png","id":"260270db-2515-470b-a730-1fe87e11d21c","prompt":"a dog riding a motorcycle","full_prompt":"a dog riding a motorcycle","percentage_complete":null,"model_name":"imagine_x_1","suggested_moderation_rule":null,"r_rated":false}
Received message: {"type":"image","blob":"...","job_id":"260270db-2515-470b-a730-1fe87e11d21c","grid_index":0,"is_alteration":false,"order":0,"section_id":0,"request_id":"57b94c87-ae13-4ade-8fc0-ebd39fb511ec","url":"https://imagine-public.x.ai/imagine-public/images/260270db-2515-470b-a730-1fe87e11d21c.png","id":"260270db-2515-470b-a730-1fe87e11d21c","prompt":"a dog riding a motorcycle","full_prompt":"A dog sits in a motorcycle sidecar on a paved road with a natural background.","percentage_complete":50.0,"model_name":"imagine_x_1","suggested_moderation_rule":null,"r_rated":false}
Received message: {"type":"image","blob":"...","job_id":"260270db-2515-470b-a730-1fe87e11d21c","grid_index":0,"is_alteration":false,"order":0,"section_id":0,"request_id":"57b94c87-ae13-4ade-8fc0-ebd39fb511ec","url":"https://imagine-public.x.ai/imagine-public/images/260270db-2515-470b-a730-1fe87e11d21c.png","id":"260270db-2515-470b-a730-1fe87e11d21c","prompt":"a dog riding a motorcycle","full_prompt":"A dog sits in a motorcycle sidecar on a paved road with a natural background.","percentage_complete":100.0,"model_name":"imagine_x_1","suggested_moderation_rule":"MODERATION_RULE_DEFAULT","r_rated":false}
@@ -0,0 +1,2 @@
{"type":"json","current_status":"start_stage","percentage_complete":0.0,"job_id":"63bb4956-7cfb-4a51-aebf-29ca29a96ad2","elapsed":null,"prompt":"a tornado hitting the stadium","confirmation":"Drew a tornado hitting the stadium","full_prompt":"A dramatic storm with a tornado near an empty stadium, in a realistic photographic style.","flags":null,"is_alteration":false,"section_id":0,"input_type":"text","request_id":"f7f7e897-8030-4163-bf54-abf421d06529","asr_transcript":null,"asr_accepted":null,"seed":null,"image_reference":null,"is_image_edit":null,"moderated":false,"r_rated":null,"order":0,"model_name":"imagine_x_1","image_id":null}
@@ -0,0 +1,125 @@
use crate::error::grok_client_error::GrokClientError;
use crate::error::grok_error::GrokError;
use crate::requests::image_websocket::messages::websocket_server_message::WebsocketServerMessage;
use futures::stream::FusedStream;
use futures::{Stream, TryStreamExt};
use log::{info, warn};
use serde::Serialize;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use wreq::ws::message::Message;
use wreq::ws::WebSocket;
#[derive(Clone)]
pub struct ClonableWebsocket {
pub(crate) websocket: Arc<RwLock<WebSocket>>,
}
impl ClonableWebsocket {
pub fn new(websocket: WebSocket) -> Self {
Self {
websocket: Arc::new(RwLock::new(websocket)),
}
}
pub async fn send(&self, message: String) -> Result<(), GrokError> {
match self.websocket.write() {
Err(err) => Err(GrokClientError::WebsocketLockError.into()),
Ok(mut websocket) => {
let message = Message::text(message);
websocket.send(message)
.await
.map_err(|err| {
GrokClientError::WreqClientError(err)
})?;
Ok(())
},
}
}
pub async fn send_serializable<T: Serialize>(&self, message: T) -> Result<(), GrokError> {
let message_json = serde_json::to_string(&message)
.map_err(|err| {
warn!("Failed to serialize prompt websocket message: {}", err);
GrokClientError::WebsocketRequestSerializationError(err)
})?;
self.send(message_json).await
}
pub async fn get_response_with_timeout(&self, duration: Duration) -> Result<Option<WebsocketServerMessage>, GrokError> {
match self.websocket.write() {
Err(_) => Err(GrokClientError::WebsocketLockError.into()),
Ok(mut websocket) => {
let result = tokio::time::timeout(duration, websocket.try_next()).await;
match result {
Err(elapsed) => {
info!("Websocket try_next() elapsed without receiving a message: {:?}", elapsed);
Ok(None) // Timeout elapsed.
}
Ok(inner) => {
let maybe_message = inner.map_err(GrokClientError::WebsocketReadError)?;
match maybe_message {
None => Ok(None),
Some(Message::Text(text)) => {
let maybe_message = WebsocketServerMessage::from_json_str(&text)?;
Ok(Some(maybe_message))
},
Some(_) => {
warn!("Received non-text websocket message.");
Ok(None)
},
}
}
}
},
}
}
pub async fn try_next_timeout(&self, duration: Duration) -> Result<Option<Message>, GrokClientError> {
match self.websocket.write() {
Err(_) => Err(GrokClientError::WebsocketLockError),
Ok(mut websocket) => {
let result = tokio::time::timeout(duration, websocket.try_next()).await;
match result {
Err(elapsed) => {
info!("Websocket try_next() elapsed without receiving a message: {:?}", elapsed);
Ok(None) // Timeout elapsed.
}
Ok(inner) => {
let maybe_message = inner.map_err(GrokClientError::WebsocketReadError)?;
Ok(maybe_message)
}
}
},
}
}
// pub async fn is_terminated(&self) -> Result<bool, GrokClientError> {
// match self.websocket.read() {
// Err(_) => Err(GrokClientError::WebsocketLockError.into()),
// Ok(websocket) => Ok(websocket.is_terminated()),
// }
// }
// /// NB: This will block forever if there is no item.
// pub async fn try_next(&self) -> Result<Option<Message>, GrokClientError> {
// match self.websocket.write() {
// Err(_) => Err(GrokClientError::WebsocketLockError),
// Ok(mut websocket) => websocket
// .try_next()
// .await
// .map_err(|err| GrokClientError::WebsocketReadError(err)),
// }
// }
// pub fn size_hint(&self) -> Result<(usize, Option<usize>), GrokClientError> {
// match self.websocket.read() {
// Err(_) => Err(GrokClientError::WebsocketLockError),
// Ok(websocket) => Ok(websocket.size_hint())
// }
// }
}
@@ -0,0 +1,106 @@
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::error::grok_generic_api_error::GrokGenericApiError;
use cloudflare_mitigation::headers::firefox_websocket_http_1_1_headers::get_firefox_websocket_http_1_1_headers;
use std::ops::{Deref, DerefMut};
use std::time::Duration;
use tokio::io::AsyncReadExt;
use wreq::header::{ACCEPT, ACCEPT_LANGUAGE, CACHE_CONTROL, COOKIE, ORIGIN, PRAGMA, SEC_WEBSOCKET_EXTENSIONS, USER_AGENT};
use wreq::ws::WebSocket;
use wreq::Client;
use wreq_util::Emulation;
const WEBSOCKET_URL: &str = "wss://grok.com/ws/imagine/listen";
pub struct CreateListenWebsocketArgs<'a> {
pub cookies: &'a str,
}
pub async fn create_listen_websocket(args: CreateListenWebsocketArgs<'_>) -> Result<WebSocket, GrokError> {
let mut client_builder = Client::builder()
.emulation(Emulation::Firefox143)
.connection_verbose(true)
.connect_timeout(Duration::from_secs(30));
let client = client_builder
.build()
.map_err(|err| GrokClientError::WreqClientError(err))?;
// NB: We need to be careful with this because HTTP/1.1 WebSocket handshake is sensitive to header casing and order.
// Cloudflare doesn't like lower case header names, and we're sending headers in the right order just to be safe.
let builder = client.websocket(WEBSOCKET_URL)
.default_headers(false)
.orig_headers(get_firefox_websocket_http_1_1_headers())
.header(COOKIE, args.cookies)
.header(ORIGIN,"https://grok.com")
.header(USER_AGENT, FIREFOX_143_MAC_USER_AGENT)
.header(SEC_WEBSOCKET_EXTENSIONS, "permessage-deflate")
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
.header("Sec-Fetch-Dest", "empty")
.header("Sec-Fetch-Mode", "websocket")
.header("Sec-Fetch-Site", "same-origin")
.header(PRAGMA, "no-cache")
.header(CACHE_CONTROL, "no-cache");
let mut response = builder.send()
.await
.map_err(|err| GrokClientError::WreqClientError(err))?;
let status = response.status();
match status.as_u16() {
101 => {
// Successful WebSocket upgrade ... fallthrough.
}
401 => {
// 401 is a likely authentication error with Grok itself.
// Unfortunately we can't read the response body text because wreq wants to consume the Response to get it, but the
// WebsocketResponse type wraps and doesn't let us move it. It's wreq's fault.
return Err(GrokError::ApiGeneric(GrokGenericApiError::LikelyWebsocketAuthentication401));
}
403 => {
// 403 is likely a cloudflare error
// Unfortunately we can't read the response body text because wreq wants to consume the Response to get it, but the
// WebsocketResponse type wraps and doesn't let us move it. It's wreq's fault.
return Err(GrokError::ApiGeneric(GrokGenericApiError::LikelyWebsocketCloudflare403));
}
_ => {
return Err(GrokError::ApiGeneric(GrokGenericApiError::UnexpectedWebsocketUpgradeStatusCode(status)));
},
}
let websocket = response.into_websocket()
.await
.map_err(|err| GrokGenericApiError::WreqWebsocketUpgradeError(err))?;
Ok(websocket)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::get_test_cookies::get_test_cookies;
use crate::test_utils::setup_test_logging::setup_test_logging;
use errors::AnyhowResult;
use log::LevelFilter;
#[tokio::test]
#[ignore] // manually test
async fn create() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Trace);
//let cookies = SESSION_COOKIES_WITHOUT_CF_CLEARANCE;
let cookies = get_test_cookies()?;
let args = CreateListenWebsocketArgs {
cookies: &cookies,
};
let result = create_listen_websocket(args).await;
if let Err(err) = result {
println!("Error: {:?}", err);
}
log::logger().flush();
assert_eq!(1, 2);
Ok(())
}
}
@@ -0,0 +1,172 @@
use crate::datatypes::api::request_id::RequestId;
use crate::error::grok_client_error::GrokClientError;
use crate::error::grok_error::GrokError;
use crate::requests::image_websocket::clonable_websocket::ClonableWebsocket;
use crate::requests::image_websocket::messages::message_image_data::MessageImageData;
use crate::utils::scrub_blobs_for_debug_logging::scrub_blobs_for_debug_logging;
use log::info;
use std::time::{Duration, Instant};
use wreq::ws::message::Message;
// Number of images we get per prompt by default
const DEFAULT_IMAGE_COUNT: usize = 6;
pub struct ListenForWebsocketImagesArgs<'a> {
pub websocket: &'a ClonableWebsocket,
pub timeout: Duration,
}
#[derive(Clone, Debug)]
pub struct ImageResults {
pub images: Vec<ImageData>,
}
#[derive(Clone, Debug)]
pub struct ImageData {
/// The "task" that generated the image
/// Multiple images have the same "task"
pub request_id: RequestId,
/// Url where we can download the image.
pub url: String,
/// The user-input prompt
pub user_prompt: String,
/// An X.ai-enriched prompt
pub enriched_prompt: String,
}
pub async fn listen_for_websocket_images(args: ListenForWebsocketImagesArgs<'_>) -> Result<ImageResults, GrokError> {
let start = Instant::now();
let end_at = start.checked_add(args.timeout);
let end_at = match end_at {
Some(end_at) => end_at,
None => return Err(GrokClientError::TimeoutMathBroken.into()),
};
let mut images = Vec::new();
loop {
if end_at < Instant::now() {
break;
}
let maybe_message = args.websocket.try_next_timeout(Duration::from_millis(500)).await?;
let maybe_message = match maybe_message {
None => None,
Some(Message::Text(text)) => Some(text.to_string()),
Some(_) => {
// Got a ping, pong, binary payload, etc.
None
},
};
let message = match maybe_message {
Some(message) => message,
None => continue,
};
let maybe_image_data = serde_json::from_str::<MessageImageData>(&message);
let image_data = match maybe_image_data {
Ok(image_data) => image_data,
Err(_err) => {
let message = scrub_blobs_for_debug_logging(&message);
println!("Unknown message: {}", message);
// NB: Might not have been a parse error, since we're parsing irrelevant message payloads too.
// This is slightly dangerous to do this way as we might mask true parsing errors with image payloads.
continue;
}
};
match image_data.percentage_complete {
None => continue,
Some(percent) => {
if percent < 100.0 {
continue;
}
}
}
images.push(ImageData {
request_id: RequestId(image_data.request_id),
url : image_data.url,
enriched_prompt: image_data.full_prompt,
user_prompt: image_data.prompt,
});
if images.len() >= DEFAULT_IMAGE_COUNT {
info!("{} images generated; we're done polling.", images.len());
break;
}
}
Ok(ImageResults {
images,
})
}
#[cfg(test)]
mod tests {
use crate::requests::image_websocket::clonable_websocket::ClonableWebsocket;
use crate::requests::image_websocket::create_listen_websocket::{create_listen_websocket, CreateListenWebsocketArgs};
use crate::requests::image_websocket::listen_for_websocket_images::{listen_for_websocket_images, ListenForWebsocketImagesArgs};
use crate::requests::image_websocket::prompt_websocket_image::{prompt_websocket_image, PromptWebsocketImageArgs};
use crate::test_utils::get_test_cookies::get_test_cookies;
use crate::test_utils::setup_test_logging::setup_test_logging;
use errors::AnyhowResult;
use log::LevelFilter;
use std::io::Write;
use std::time::Duration;
#[tokio::test]
#[ignore] // manually test
async fn prompt() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Info);
let prompt = "A tornado hitting the stadium";
let cookies = get_test_cookies()?;
let websocket = create_listen_websocket(CreateListenWebsocketArgs {
cookies: &cookies,
}).await?;
let websocket = ClonableWebsocket::new(websocket);
println!("Sending...");
std::io::stdout().flush()?;
let _result = prompt_websocket_image(PromptWebsocketImageArgs {
websocket_wrapped: websocket.clone(),
prompt,
}).await?;
println!("Reading...");
std::io::stdout().flush()?;
println!("Polling.");
let images = listen_for_websocket_images(ListenForWebsocketImagesArgs {
websocket: &websocket,
timeout: Duration::from_millis(10_000),
}).await?;
println!("Done polling. Images: {}", images.images.len());
for image in images.images {
println!("Image: {:?}", image);
}
log::logger().flush();
assert_eq!(1,2);
Ok(())
}
}
@@ -0,0 +1,124 @@
use crate::datatypes::api::request_id::RequestId;
use crate::error::grok_client_error::GrokClientError;
use crate::error::grok_error::GrokError;
use crate::requests::image_websocket::clonable_websocket::ClonableWebsocket;
use crate::requests::image_websocket::messages::message_request_id::MessageRequestId;
use std::time::{Duration, Instant};
use wreq::ws::message::Message;
pub struct ListenForWebsocketRequestIdArgs<'a> {
pub websocket: &'a ClonableWebsocket,
pub timeout: Duration,
}
#[derive(Clone, Debug)]
pub struct RequestIdResult {
pub request_id: Option<RequestId>,
}
pub async fn listen_for_websocket_request_id(args: ListenForWebsocketRequestIdArgs<'_>) -> Result<RequestIdResult, GrokError> {
let start = Instant::now();
let end_at = start.checked_add(args.timeout);
let end_at = match end_at {
Some(end_at) => end_at,
None => return Err(GrokClientError::TimeoutMathBroken.into()),
};
let mut request_id = None;
loop {
if end_at < Instant::now() {
break;
}
let maybe_message = args.websocket.try_next_timeout(Duration::from_millis(500)).await?;
let maybe_message = match maybe_message {
None => None,
Some(Message::Text(text)) => Some(text.to_string()),
Some(_) => {
// Got a ping, pong, binary payload, etc.
None
},
};
let message = match maybe_message {
Some(message) => message,
None => continue,
};
let maybe_parsed = serde_json::from_str::<MessageRequestId>(&message);
let parsed = match maybe_parsed {
Ok(parsed) => parsed,
Err(_err) => {
// NB: Might not have been a parse error, since we're parsing irrelevant message payloads too.
// This is slightly dangerous to do this way as we might mask true parsing errors with image payloads.
continue;
}
};
request_id = Some(RequestId(parsed.request_id));
break;
}
Ok(RequestIdResult {
request_id,
})
}
#[cfg(test)]
mod tests {
use crate::requests::image_websocket::clonable_websocket::ClonableWebsocket;
use crate::requests::image_websocket::create_listen_websocket::{create_listen_websocket, CreateListenWebsocketArgs};
use crate::requests::image_websocket::listen_for_websocket_request_id::{listen_for_websocket_request_id, ListenForWebsocketRequestIdArgs};
use crate::requests::image_websocket::prompt_websocket_image::{prompt_websocket_image, PromptWebsocketImageArgs};
use crate::test_utils::get_test_cookies::get_test_cookies;
use crate::test_utils::setup_test_logging::setup_test_logging;
use errors::AnyhowResult;
use log::LevelFilter;
use std::io::Write;
use std::time::Duration;
#[tokio::test]
#[ignore] // manually test
async fn prompt() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Info);
let prompt = "A tiny tornado on a desk";
let cookies = get_test_cookies()?;
let websocket = create_listen_websocket(CreateListenWebsocketArgs {
cookies: &cookies,
}).await?;
let websocket = ClonableWebsocket::new(websocket);
println!("Sending...");
std::io::stdout().flush()?;
let _result = prompt_websocket_image(PromptWebsocketImageArgs {
websocket_wrapped: websocket.clone(),
prompt,
}).await?;
println!("Reading...");
std::io::stdout().flush()?;
println!("Polling.");
let request_data = listen_for_websocket_request_id(ListenForWebsocketRequestIdArgs {
websocket: &websocket,
timeout: Duration::from_millis(10_000),
}).await?;
println!("Done polling. Request Data: {:?}", request_data);
log::logger().flush();
assert_eq!(1,2);
Ok(())
}
}
@@ -0,0 +1,63 @@
use serde::Deserialize;
/// Images with a binary blob and URLs.
/// We may receive several of these for a single prompt.
#[derive(Deserialize, Clone, Debug)]
pub struct MessageImageData {
/// UUID.
/// Probably an identifier for the original request/prompt.
pub request_id: String, // Option<String>,
/// UUID.
/// This turns out to be the file ID in the image URL (and `job_id`).
pub id: Option<String>,
/// UUID.
/// This turns out to be the file ID in the image URL (and `id`).
pub job_id: Option<String>,
/// Eg. null, "0", "50", "100"
pub percentage_complete: Option<f32>,
/// The user's prompt
pub prompt: String, //pub prompt: Option<String>,
/// The X.ai enriched prompt
pub full_prompt: String, //pub full_prompt: Option<String>,
/// URL to the image.
pub url: String, //pub url: Option<String>,
// We don't need to decode - let's just download again
// /// Base64 encoded image blob.
// pub blob: Option<String>,
// /// NSFW flag
// pub r_rated: Option<bool>,
// /// Name of the model used to generate the image
// /// eg. "imagine_h_1", "imagine_x_1"
// pub model_name: Option<String>,
}
/*
Received websocket message: {
"type": "image",
"blob": "...",
"job_id": "260270db-2515-470b-a730-1fe87e11d21c",
"grid_index": 0,
"is_alteration": false,
"order": 0,
"section_id": 0,
"request_id": "57b94c87-ae13-4ade-8fc0-ebd39fb511ec",
"url": "https://imagine-public.x.ai/imagine-public/images/260270db-2515-470b-a730-1fe87e11d21c.png",
"id": "260270db-2515-470b-a730-1fe87e11d21c",
"prompt": "a dog riding a motorcycle",
"full_prompt": "A dog sits in a motorcycle sidecar on a paved road with a natural background.",
"percentage_complete": 100.0,
"model_name": "imagine_x_1",
"suggested_moderation_rule": "MODERATION_RULE_DEFAULT",
"r_rated":false
}
*/
@@ -0,0 +1,11 @@
use serde::Deserialize;
/// This could intercept any message with a "request_id"
/// We're going to try to use these to end the image prompting early.
/// If we're not careful, we might accidentally ingest completed image messages this way.
#[derive(Deserialize, Clone, Debug)]
pub struct MessageRequestId {
/// UUID.
/// Probably an identifier for the original request/prompt.
pub request_id: String, // Option<String>,
}
@@ -0,0 +1,4 @@
pub mod message_image_data;
pub mod message_request_id;
pub mod websocket_client_message;
pub mod websocket_server_message;
@@ -0,0 +1,76 @@
use chrono::Utc;
use serde::Serialize;
use uuid::Uuid;
#[derive(Serialize)]
pub struct WebsocketClientMessage {
/// Type of message
/// eg. "conversation.item.create"
pub r#type: String,
/// Unix timestamp
pub timestamp: u64,
pub item: ClientMessageItem,
}
#[derive(Serialize)]
pub struct ClientMessageItem {
/// Type of item
/// eg. "message"
pub r#type: String,
pub content: Vec<ClientMessageItemContent>,
}
#[derive(Serialize)]
pub struct ClientMessageItemContent {
/// UUID
#[serde(rename="requestId")]
pub request_id: String,
/// Type of item content
/// eg. "input_text"
pub r#type: String,
/// The prompt for the request
pub text: String,
pub properties: ClientMessageItemContentProperties,
}
#[derive(Serialize)]
pub struct ClientMessageItemContentProperties {
pub section_count: usize,
pub is_kids_mode: bool,
pub enable_nsfw: bool,
pub skip_upsampler: bool,
pub is_initial: bool,
}
impl WebsocketClientMessage {
/// Create a new image prompt websocket client message
pub fn new_image_prompt(prompt: &str) -> Self {
Self {
r#type: "conversation.item.create".to_string(),
timestamp: Utc::now().timestamp_millis() as u64,
item: ClientMessageItem {
r#type: "message".to_string(),
content: vec![
ClientMessageItemContent {
request_id: Uuid::new_v4().to_string(),
r#type: "input_text".to_string(),
text: prompt.to_string(),
properties: ClientMessageItemContentProperties {
section_count: 0,
is_kids_mode: false,
enable_nsfw: true,
skip_upsampler: false,
is_initial: false,
},
}
],
},
}
}
}
@@ -0,0 +1,204 @@
use crate::error::grok_generic_api_error::GrokGenericApiError;
use serde::Deserialize;
#[derive(Deserialize, Clone, Debug)]
#[serde(tag = "type")]
pub enum WebsocketServerMessage {
/// Json with root `type = "image"`
#[serde(rename = "image")]
Image(ImageDataMessage),
/// Json with root `type = "json"`
#[serde(rename = "json")]
Json(JsonDataMessage),
/// Captures any other JSON messages.
#[serde(untagged)]
Unknown(serde_json::Value),
//Unknown(String),
}
/// Images with a binary blob and URLs.
/// We may receive several of these for a single prompt.
#[derive(Deserialize, Clone, Debug)]
pub struct ImageDataMessage {
/// UUID.
pub id: Option<String>,
/// UUID.
pub job_id: Option<String>,
/// UUID.
pub request_id: Option<String>,
/// Eg. "0", "50", "100"
pub percentage_complete: Option<f32>,
pub prompt: Option<String>,
/// The enriched prompt
pub full_prompt: Option<String>,
/// Base64 encoded image blob.
pub blob: Option<String>,
/// URL to the image.
pub url: Option<String>,
/// NSFW flag
pub r_rated: Option<bool>,
/// Name of the model used to generate the image
/// eg. "imagine_h_1"
pub model_name: Option<String>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct JsonDataMessage {
/// UUID.
pub job_id: Option<String>,
/// UUID.
pub request_id: Option<String>,
/// Eg. "0", "50", "100"
pub percentage_complete: Option<f32>,
pub prompt: Option<String>,
pub full_prompt: Option<String>,
/// NSFW flag
pub r_rated: Option<bool>,
/// UUID.
/// NB: This is observed to be nullable.
pub image_id: Option<String>,
}
impl WebsocketServerMessage {
pub fn from_json_str(json_str: &str) -> Result<Self, GrokGenericApiError> {
Ok(serde_json::from_str(json_str)
.map_err(|err| GrokGenericApiError::SerdeResponseParseErrorWithBody(err, json_str.to_string()))?)
}
}
#[cfg(test)]
mod tests {
use crate::requests::image_websocket::messages::websocket_server_message::WebsocketServerMessage;
fn json_path(file_name: &str) -> String {
format!("/Users/bt/dev/storyteller/storyteller-rust/crates/api_clients/grok_client/test_data/websocket_messages/{}", file_name)
}
#[test]
fn test_image_complete() -> anyhow::Result<()> {
let path = json_path("create_image_complete_response.json");
let data = std::fs::read_to_string(path)?;
//let message: WebsocketServerMessage = serde_json::from_str(&data)?;
let message = WebsocketServerMessage::from_json_str(&data)?;
match message {
WebsocketServerMessage::Image(image_data) => {
assert_eq!(image_data.percentage_complete, Some(50.0));
assert!(image_data.url.is_some());
},
_ => panic!("Expected ImageData message"),
}
Ok(())
}
#[test]
fn test_image_status_complete() -> anyhow::Result<()> {
let path = json_path("create_image_complete_status_notification.json");
let data = std::fs::read_to_string(path)?;
//let message: WebsocketServerMessage = serde_json::from_str(&data)?;
let message = WebsocketServerMessage::from_json_str(&data)?;
match message {
WebsocketServerMessage::Json(image_data) => {
assert_eq!(image_data.percentage_complete, Some(100.0));
},
_ => panic!("Expected JsonData message"),
}
Ok(())
}
#[test]
fn test_in_progress() -> anyhow::Result<()> {
let path = json_path("create_image_in_progress_response.json");
let data = std::fs::read_to_string(path)?;
//let message: WebsocketServerMessage = serde_json::from_str(&data)?;
let message = WebsocketServerMessage::from_json_str(&data)?;
match message {
WebsocketServerMessage::Json(image_data) => {
assert_eq!(image_data.percentage_complete, Some(0.0));
},
_ => panic!("Expected JsonData message"),
}
Ok(())
}
#[test]
fn test_wire() -> anyhow::Result<()> {
let path = json_path("wire.json");
let data = std::fs::read_to_string(path)?;
//let message: WebsocketServerMessage = serde_json::from_str(&data)?;
let message = WebsocketServerMessage::from_json_str(&data)?;
match message {
WebsocketServerMessage::Json(_value)=> {},
_ => panic!("Expected Json message"),
}
Ok(())
}
#[test]
fn test_wire_formatted() -> anyhow::Result<()> {
let path = json_path("wire_formatted.json");
let data = std::fs::read_to_string(path)?;
//let message: WebsocketServerMessage = serde_json::from_str(&data)?;
let message = WebsocketServerMessage::from_json_str(&data)?;
match message {
WebsocketServerMessage::Json(_value)=> {},
_ => panic!("Expected Json message"),
}
Ok(())
}
#[test]
fn test_empty() -> anyhow::Result<()> {
let path = json_path("empty.json");
let data = std::fs::read_to_string(path)?;
//let message: WebsocketServerMessage = serde_json::from_str(&data)?;
let message = WebsocketServerMessage::from_json_str(&data)?;
match message {
WebsocketServerMessage::Unknown(_value)=> {},
_ => panic!("Expected Unknown message"),
}
Ok(())
}
#[test]
fn test_other_type() -> anyhow::Result<()> {
let path = json_path("other_type.json");
let data = std::fs::read_to_string(path)?;
///let message: WebsocketServerMessage = serde_json::from_str(&data)?;
let message = WebsocketServerMessage::from_json_str(&data)?;
match message {
WebsocketServerMessage::Unknown(_value)=> {},
_ => panic!("Expected Unknown message"),
}
Ok(())
}
}
@@ -0,0 +1,6 @@
pub mod clonable_websocket;
pub mod create_listen_websocket;
pub mod listen_for_websocket_images;
pub mod listen_for_websocket_request_id;
pub mod messages;
pub mod prompt_websocket_image;
@@ -0,0 +1,166 @@
use crate::error::grok_error::GrokError;
use crate::requests::image_websocket::clonable_websocket::ClonableWebsocket;
use crate::requests::image_websocket::messages::websocket_client_message::WebsocketClientMessage;
pub struct PromptWebsocketImageArgs<'a> {
pub websocket_wrapped: ClonableWebsocket,
pub prompt: &'a str,
}
pub async fn prompt_websocket_image(args: PromptWebsocketImageArgs<'_>) -> Result<(), GrokError> {
let message = WebsocketClientMessage::new_image_prompt(args.prompt);
args.websocket_wrapped.send_serializable(&message).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use crate::requests::image_websocket::clonable_websocket::ClonableWebsocket;
use crate::requests::image_websocket::create_listen_websocket::{create_listen_websocket, CreateListenWebsocketArgs};
use crate::requests::image_websocket::messages::websocket_server_message::WebsocketServerMessage;
use crate::requests::image_websocket::prompt_websocket_image::{prompt_websocket_image, PromptWebsocketImageArgs};
use crate::test_utils::get_test_cookies::get_test_cookies;
use errors::AnyhowResult;
use log::warn;
use std::io::Write;
use std::time::Duration;
use wreq::ws::message::Message;
#[tokio::test]
#[ignore] // manually test
async fn prompt() -> AnyhowResult<()> {
//setup_test_logging(LevelFilter::Trace);
let cookies = get_test_cookies()?;
let websocket = create_listen_websocket(CreateListenWebsocketArgs {
cookies: &cookies,
}).await?;
let websocket = ClonableWebsocket::new(websocket);
println!("Sending...");
std::io::stdout().flush()?;
let result = prompt_websocket_image(PromptWebsocketImageArgs {
websocket_wrapped: websocket.clone(),
prompt: "a dog riding a motorcycle",
}).await?;
println!("Reading...");
std::io::stdout().flush()?;
let mut count = 0;
println!("Polling.");
//while let Some(message) = websocket.try_next().await? {
// println!("[{count}] Received websocket message: {:?}", message);
// count = count + 1;
// tokio::time::sleep(Duration::from_millis(1000)).await;
// if count > 1000 {
// break;
// }
//}
//loop {
// count = count + 1;
// let bounds = websocket.size_hint()?;
// println!("Websocket size hint: {:?}", bounds);
// tokio::time::sleep(Duration::from_millis(1000)).await;
// if count > 30 {
// break;
// }
//}
//loop {
// let maybe_message =
// websocket.try_next_timeout(Duration::from_millis(1000)).await?;
// match maybe_message {
// None => {
// println!("No message received within timeout.");
// count = count + 1;
// }
// Some(message) => {
// println!("[{count}] Received websocket message!");//: {:?}", message);
// count = 0;
// }
// }
// if count > 5 {
// println!("No messages after 5 seconds");
// break;
// }
//}
loop {
//let maybe_message =
// websocket.get_response_with_timeout(Duration::from_millis(1000)).await?;
let maybe_message =
websocket.try_next_timeout(Duration::from_millis(1000)).await?;
let mut maybe_raw_text : Option<String> = None;
let maybe_message = match maybe_message {
None => None,
Some(Message::Text(text)) => {
let maybe_message = WebsocketServerMessage::from_json_str(text.as_str())?;
let mut text = text.as_str().to_string();
text.truncate(100);
//println!("Received message: {}", text);
maybe_raw_text = Some(text);
Some(maybe_message)
},
Some(_) => {
warn!("Received non-text websocket message.");
None
},
};
match maybe_message {
None => {
println!("No message received within timeout.");
count = count + 1;
}
Some(message) => {
match message {
WebsocketServerMessage::Image(image) => {
println!("IMAGE: {:?}", image.percentage_complete);
}
WebsocketServerMessage::Json(json) => {
println!("JSON : {:?}", json.percentage_complete);
}
WebsocketServerMessage::Unknown(unknown) => {
//let typ = unknown.get("type");
let unknown_string = unknown.as_str();
println!("[UNKNOWN] websocket message: {:?}", maybe_raw_text);
}
}
count = 0;
}
}
// TODO: Make sure we capture all the events for images
// TODO: Explore the other APIs
// TODO understand the IDs
// TODO: Video websocket APIs
// TODO: Upload APIs
if count > 5 {
println!("No messages after 5 seconds");
break;
}
}
println!("Done polling.");
log::logger().flush();
assert_eq!(1,2);
Ok(())
}
}
@@ -0,0 +1,13 @@
curl 'https://grok.com/_next/static/chunks/dcdf7d6886d5ad11.js' \
--compressed \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' \
-H 'Accept: */*' \
-H 'Accept-Language: en-US,en;q=0.5' \
-H 'Accept-Encoding: gzip, deflate, br, zstd' \
-H 'Referer: https://grok.com/' \
-H 'Connection: keep-alive' \
-H 'Cookie: stblid=4c0f7d0f-9768-49cf-94f7-1f59f1adf1f5; x-anon-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNmFhNTFmZmYtMmFlNC00NjU3LTk0ZDItNDFjMWQyMjNlZTUyIiwidGVhbV9pZCI6bnVsbCwiZXhwIjoxNzYwMDQyNDA0LCJpc19hbm9uX3VzZXIiOnRydWV9.ySBr18x0Tht-a3RE-cdtQDbWoyJ1SbB_JrHAnNSFzOM; cf_clearance=KkJVT51OluKzLC1Hv7OFT.BAKyB4cmjqGaui9Q.KaJI-1761037978-1.2.1.1-HcSAp01jVBujvTt86Dpe5zNawFRHE0rfVhJ_46_15kSo1_aEl14trWrDDnZeSuZk2CWFfE8kvn93hSugxf73emKqNk25YDcDIPK_077hYMuFsJAWqK4Pj5V9b3KRcB.oYmiY5zaf8yUf2XQ0hC09HCPp9DtwlwAfbZ9DKpKmHWgRs62uZzGNb_AIItONxkJLCp0L3iI6BbMdyDzcf.NCzaqtSmuKA5JFybWmYP2y7Ec; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiM2Y3NDgzMTYtOTkyMC00ZTkzLTk1NzQtOTA3ZGNkYzU4M2M5In0.9ne0Ab4cJiNMmntjQwxIfCrVcY6qmuSYMiNRSph1Y60; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiM2Y3NDgzMTYtOTkyMC00ZTkzLTk1NzQtOTA3ZGNkYzU4M2M5In0.9ne0Ab4cJiNMmntjQwxIfCrVcY6qmuSYMiNRSph1Y60; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%222613c794-fce7-4ade-9b22-6118f977ede8%22%2C%22%24device_id%22%3A%22c3c89bdd-b1ba-4939-9ab4-b03b4dfd1e60%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%222613c794-fce7-4ade-9b22-6118f977ede8%22%2C%22%24search_engine%22%3A%22google%22%2C%22utm_source%22%3A%22x%22%2C%22utm_campaign%22%3A%22grok_imagine_post%22%7D' \
-H 'Sec-Fetch-Dest: script' \
-H 'Sec-Fetch-Mode: no-cors' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'TE: trailers'
@@ -0,0 +1,102 @@
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::error::grok_generic_api_error::GrokGenericApiError;
use log::info;
use std::ops::Deref;
use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, ORIGIN, PRAGMA, REFERER, TE, UPGRADE, USER_AGENT};
use wreq::ws::message::Message;
use wreq::Client;
use wreq_util::Emulation;
const INDEX_URL: &str = "https://grok.com";
pub struct GetIndexPageArgs<'a> {
pub cookie: &'a str,
}
pub struct IndexPage {
pub body: String,
}
pub async fn get_index(args: GetIndexPageArgs<'_>) -> Result<IndexPage, GrokError> {
info!("Building client...");
let client = Client::builder()
.emulation(Emulation::Firefox143)
.build()
.map_err(|err| GrokClientError::WreqClientError(err))?;
info!("Configuring client...");
let builder = client.get(INDEX_URL)
.header("User-Agent", FIREFOX_143_MAC_USER_AGENT)
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.header("Accept-Encoding", "gzip, deflate, br, zstd")
.header("Accept-Language", "en-US,en;q=0.5")
.header("Connection", "keep-alive")
.header("Sec-Fetch-Dest", "document")
.header("Sec-Fetch-Mode", "navigate")
.header("Sec-Fetch-Site", "none")
.header("Sec-Fetch-User", "?1")
.header("Sec-GPC", "1")
.header("priority", "u=0, i")
.header("Pragma", "no-cache")
.header("Cache-Control", "no-cache")
.header("Cookie", args.cookie.to_string())
.header("TE", "trailers");
info!("Sending request for index page...");
let response = builder.send()
.await
.map_err(|err| GrokClientError::WreqClientError(err))?;
let status = response.status();
println!("Status: {}", status);
//let body = response.text()
// .await
// .map_err(|err| GrokGenericApiError::WreqError(err))?;
// stblid=08552693-0377-49d3-b17f-8e4c68b153ec
for cookie in response.cookies() {
println!("Cookie: {}={}", cookie.name(), cookie.value());
}
for (name, value) in response.headers() {
println!("Header: {}: {}", name.as_str(), value.to_str().unwrap());
}
let body = response.text()
.await
.map_err(|err| GrokClientError::WreqClientError(err))?;
Ok(IndexPage {
body,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::get_test_cookies::get_test_cookies;
use crate::test_utils::setup_test_logging::setup_test_logging;
use errors::AnyhowResult;
use log::LevelFilter;
#[tokio::test]
#[ignore] // manually test
async fn test() -> AnyhowResult<()> {
setup_test_logging(LevelFilter::Trace);
let cookie = get_test_cookies()?;
let args = GetIndexPageArgs {
cookie: &cookie,
};
let result = get_index(args).await?;
println!("Body:\n\n");
println!("{}", result.body);
assert_eq!(1, 2);
Ok(())
}
}
@@ -0,0 +1,88 @@
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::index_page::parsers::index::parse_index_script_list::parse_index_svg_paths;
use crate::requests::index_page::requests::get_index_page_scripts_with_client::{get_index_page_scripts_with_client, GetIndexPageScriptsArgs};
use crate::requests::index_page::requests::get_index_page_with_client::{get_index_page_with_client, GetIndexPageWithClientArgs};
use crate::utils::create_firefox_client::create_firefox_client;
use log::info;
use std::collections::HashMap;
use wreq::Client;
use wreq_util::Emulation;
const INDEX_URL: &str = "https://grok.com";
pub struct GetIndexPageAndScriptsArgs<'a> {
pub cookie: &'a str,
}
pub struct IndexPageAndScripts {
/// Index.html
pub index_body_html: String,
/// Script path -> Javascript payload.
pub scripts: HashMap<String, String>,
// The client that we used.
pub client: Client,
}
pub async fn get_index_page_and_scripts(args: GetIndexPageAndScriptsArgs<'_>) -> Result<IndexPageAndScripts, GrokError> {
info!("Building client...");
let client = create_firefox_client()?;
info!("Fetching index page...");
let index = get_index_page_with_client(GetIndexPageWithClientArgs {
client: &client,
cookie: args.cookie,
}).await?;
let scripts = parse_index_svg_paths(&index.body);
info!("Fetching {} scripts...", scripts.len());
let scripts = get_index_page_scripts_with_client(GetIndexPageScriptsArgs {
client: &client,
cookie: &args.cookie,
scripts: &scripts,
}).await?;
Ok(IndexPageAndScripts {
index_body_html: index.body,
scripts,
client,
})
}
#[cfg(test)]
mod tests {
use crate::requests::index_page::get_index_page_and_scripts::{get_index_page_and_scripts, GetIndexPageAndScriptsArgs};
use crate::test_utils::get_test_cookies::get_test_cookies;
use errors::AnyhowResult;
#[tokio::test]
#[ignore] // manually test
async fn test() -> AnyhowResult<()> {
//setup_test_logging(LevelFilter::Trace);
let cookie = get_test_cookies()?;
let result = get_index_page_and_scripts(GetIndexPageAndScriptsArgs {
cookie: &cookie,
}).await?;
let mut truncated_body = result.index_body_html.clone();
truncated_body.truncate(500);
println!("Body:\n\n");
println!("{}", truncated_body);
for (k, v) in result.scripts.iter() {
let mut body = v.to_string();
body.truncate(100);
println!("{}: {}\n\n", k, body);
}
assert_eq!(1, 2);
Ok(())
}
}
@@ -0,0 +1,87 @@
use crate::datatypes::api::xsid_numbers::XsidNumbers;
use crate::error::grok_error::GrokError;
use crate::requests::index_page::parsers::index::parse_index_on_demand_script::parse_index_on_demand_script;
use crate::requests::index_page::parsers::script::parse_xsid_script_numbers::parse_xsid_script_numbers;
use crate::requests::index_page::requests::get_index_page_script_with_client::{get_index_page_script_with_client, GetIndexPageScriptArgs};
use crate::requests::index_page::utils::convert_verification_token_to_loading_anim::LoadingAnim;
use wreq::Client;
// self.svg_data, self.numbers = Parser.parse_values(c_request.text, self.anim, self.xsid_script)
pub struct GetNumbersArgs<'a> {
pub client: &'a Client,
pub html: &'a str,
pub cookie: &'a str,
pub loading_anim: &'a LoadingAnim,
pub xsid_script_id: &'a str,
}
pub async fn get_numbers(args: GetNumbersArgs<'_>) -> Result<XsidNumbers, GrokError> {
let script_link = match args.xsid_script_id {
"ondemand.s" => {
// TODO: Not sure this is relevant anymore. Not a full or correct implementation of this branch.
let on_demand = parse_index_on_demand_script(&args.html);
let on_demand = on_demand.unwrap_or_else(|| "".to_string());
format!("https://abs.twimg.com/responsive-web/client-web/ondemand.s.{}a.js", on_demand)
},
_ => format!("https://grok.com/_next/{}", args.xsid_script_id),
};
let xsid_script_body = get_index_page_script_with_client(GetIndexPageScriptArgs {
client: &args.client,
cookie: args.cookie,
script_url: &script_link,
}).await?;
Ok(parse_xsid_script_numbers(&xsid_script_body))
}
#[cfg(test)]
mod tests {
use crate::requests::index_page::get_index_page_and_scripts::{get_index_page_and_scripts, GetIndexPageAndScriptsArgs};
use crate::requests::index_page::get_numbers::{get_numbers, GetNumbersArgs};
use crate::requests::index_page::parsers::index::parse_index_verification_token::parse_index_verification_token;
use crate::requests::index_page::parsers::script::parse_script_actions_and_xsid_script_path::parse_script_actions_and_xsid_script_path;
use crate::requests::index_page::utils::convert_verification_token_to_loading_anim::convert_verification_token_to_loading_anim;
use crate::test_utils::get_test_cookies::get_test_cookies;
use errors::AnyhowResult;
#[tokio::test]
#[ignore] // Manual test invocation
async fn test() -> AnyhowResult<()> {
let cookie = get_test_cookies()?;
let page_and_scripts = get_index_page_and_scripts(GetIndexPageAndScriptsArgs {
cookie: &cookie,
}).await?;
let verification_token = parse_index_verification_token(&page_and_scripts.index_body_html)
.expect("expected verification token");
println!("Verification Token: {:?}", verification_token);
let loading_anim = convert_verification_token_to_loading_anim(&verification_token)?;
println!("Loading Animation: {:?}", loading_anim);
let actions_and_xsid_script = parse_script_actions_and_xsid_script_path(&page_and_scripts.scripts)?;
println!("Actions and XSID: {:?}", actions_and_xsid_script);
let result = get_numbers(GetNumbersArgs {
client: &page_and_scripts.client,
html: &page_and_scripts.index_body_html,
cookie: &cookie,
loading_anim: &loading_anim,
xsid_script_id: &actions_and_xsid_script.xsid_script_path,
}).await?;
println!("Final Numbers: {:?}", result);
println!("\n\nBody:\n\n");
println!("{}", page_and_scripts.index_body_html);
assert_eq!(1, 2);
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More