x_client_transaction/
transaction.rs

1use crate::cubic_curve::Cubic;
2use crate::error::Error;
3use crate::interpolate::interpolate;
4use crate::rotation::convert_rotation_to_matrix;
5use crate::utils::{
6    base64_decode, base64_encode, float_to_hex, handle_x_migration, is_odd, js_round,
7};
8use lazy_static::lazy_static;
9use regex::Regex;
10use reqwest::blocking::Client;
11use scraper::{Html, Selector};
12use sha2::{Digest, Sha256};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15lazy_static! {
16    static ref ON_DEMAND_FILE_REGEX: Regex =
17        Regex::new(r#"['|\"]ondemand\.s['|\"]:\s*['|\"]([\w]*)['|\"]"#).unwrap();
18    static ref INDICES_REGEX: Regex = Regex::new(r"(\(\w{1}\[(\d{1,2})\],\s*16\))+").unwrap();
19}
20
21pub struct ClientTransaction {
22    additional_random_number: u8,
23    default_keyword: String,
24    key_bytes: Vec<u8>,
25    animation_key: String,
26}
27
28impl ClientTransaction {
29    pub fn new(client: &Client) -> Result<Self, Error> {
30        let home_page = handle_x_migration(client)?;
31
32        let (row_index, key_bytes_indices) = Self::get_indices(&home_page, client)?;
33        let key = Self::get_key(&home_page)?;
34        let key_bytes = Self::get_key_bytes(&key)?;
35        let animation_key =
36            Self::get_animation_key(&key_bytes, &home_page, row_index, &key_bytes_indices)?;
37
38        Ok(Self {
39            additional_random_number: 3,
40            default_keyword: String::from("obfiowerehiring"),
41            key_bytes,
42            animation_key,
43        })
44    }
45
46    fn get_indices(home_page: &Html, client: &Client) -> Result<(usize, Vec<usize>), Error> {
47        let mut key_byte_indices = Vec::new();
48
49        // Find ondemand file
50        let html_content = home_page.html();
51        let on_demand_file = ON_DEMAND_FILE_REGEX
52            .captures(&html_content)
53            .ok_or_else(|| Error::Parse("Couldn't find ondemand file".into()))?;
54
55        let on_demand_file_url = format!(
56            "https://abs.twimg.com/responsive-web/client-web/ondemand.s.{}a.js",
57            on_demand_file.get(1).unwrap().as_str()
58        );
59
60        // Fetch ondemand file
61        let on_demand_response = client.get(&on_demand_file_url).send()?;
62        let on_demand_content = on_demand_response.text()?;
63
64        // Extract indices
65        for captures in INDICES_REGEX.captures_iter(&on_demand_content) {
66            if let Some(index_match) = captures.get(2) {
67                if let Ok(index) = index_match.as_str().parse::<usize>() {
68                    key_byte_indices.push(index);
69                }
70            }
71        }
72
73        if key_byte_indices.is_empty() {
74            return Err(Error::Parse("Couldn't get KEY_BYTE indices".into()));
75        }
76
77        Ok((key_byte_indices[0], key_byte_indices[1..].to_vec()))
78    }
79
80    fn get_key(page: &Html) -> Result<String, Error> {
81        let selector = Selector::parse("[name='twitter-site-verification']").unwrap();
82        let element = page
83            .select(&selector)
84            .next()
85            .ok_or_else(|| Error::MissingKey("Couldn't get key from the page source".into()))?;
86
87        let key = element
88            .value()
89            .attr("content")
90            .ok_or_else(|| Error::MissingKey("Missing content attribute".into()))?;
91
92        Ok(key.to_string())
93    }
94
95    fn get_key_bytes(key: &str) -> Result<Vec<u8>, Error> {
96        Ok(base64_decode(key)?)
97    }
98
99    fn get_frames(page: &Html) -> Vec<scraper::ElementRef> {
100        let selector = Selector::parse("[id^='loading-x-anim']").unwrap();
101        page.select(&selector).collect()
102    }
103
104    fn get_2d_array(
105        key_bytes: &[u8],
106        page: &Html,
107        frames: Option<Vec<scraper::ElementRef>>,
108    ) -> Result<Vec<Vec<i32>>, Error> {
109        let frames = frames.unwrap_or_else(|| Self::get_frames(page));
110
111        let frame_index = (key_bytes[5] % 4) as usize;
112        if frame_index >= frames.len() {
113            return Err(Error::Parse("Invalid frame index".into()));
114        }
115
116        let frame = frames[frame_index];
117
118        let mut outer_children = frame.children();
119        let first_child = outer_children
120            .next()
121            .ok_or_else(|| Error::Parse("No first child in frame".into()))?;
122        let first_child = scraper::ElementRef::wrap(first_child)
123            .ok_or_else(|| Error::Parse("First child is not an element".into()))?;
124
125        let mut inner_children = first_child.children();
126        let path_node = inner_children
127            .nth(1)
128            .ok_or_else(|| Error::Parse("No second child in an inner group".into()))?;
129        let path_elem = scraper::ElementRef::wrap(path_node)
130            .ok_or_else(|| Error::Parse("Second child is not an element".into()))?;
131
132        let d_attr = path_elem
133            .value()
134            .attr("d")
135            .ok_or_else(|| Error::Parse("Missing 'd' attribute".into()))?;
136        let d_content = d_attr
137            .get(9..)
138            .ok_or_else(|| Error::Parse("Path data too short".into()))?;
139
140        let segments = d_content.split('C');
141
142        let mut result = Vec::new();
143        for segment in segments {
144            let numbers: Vec<i32> = segment
145                .replace(|c: char| !c.is_ascii_digit() && c != '-', " ")
146                .split_whitespace()
147                .filter_map(|s| s.parse::<i32>().ok())
148                .collect();
149
150            result.push(numbers);
151        }
152
153        Ok(result)
154    }
155
156    fn solve(value: f64, min_val: f64, max_val: f64, rounding: bool) -> f64 {
157        let result = value * (max_val - min_val) / 255.0 + min_val;
158        if rounding {
159            result.floor()
160        } else {
161            (result * 100.0).round() / 100.0
162        }
163    }
164
165    fn animate(frames: &[i32], target_time: f64) -> String {
166        let from_color: Vec<f64> = frames[..3]
167            .iter()
168            .map(|&i| i as f64)
169            .chain(std::iter::once(1.0))
170            .collect();
171        let to_color: Vec<f64> = frames[3..6]
172            .iter()
173            .map(|&i| i as f64)
174            .chain(std::iter::once(1.0))
175            .collect();
176        let from_rotation = vec![0.0];
177        let to_rotation = vec![Self::solve(frames[6] as f64, 60.0, 360.0, true)];
178
179        let curves: Vec<f64> = frames[7..]
180            .iter()
181            .enumerate()
182            .map(|(i, &val)| Self::solve(val as f64, is_odd(i as i32), 1.0, false))
183            .collect();
184
185        let cubic = Cubic::new(curves);
186        let val = cubic.get_value(target_time);
187
188        let color = interpolate(&from_color, &to_color, val).unwrap();
189        let color: Vec<f64> = color.iter().map(|&v| v.clamp(0.0, 255.0)).collect();
190
191        let rotation = interpolate(&from_rotation, &to_rotation, val).unwrap();
192        let matrix = convert_rotation_to_matrix(rotation[0]);
193
194        let mut str_arr = Vec::new();
195
196        // Add color values as hex
197        for value in &color[..color.len() - 1] {
198            str_arr.push(format!("{:x}", value.round() as i32));
199        }
200
201        // Add matrix values as hex
202        for value in matrix {
203            let rounded = (value * 100.0).round() / 100.0;
204            let abs_value = rounded.abs();
205            let hex_value = float_to_hex(abs_value);
206
207            if hex_value.starts_with('.') {
208                str_arr.push(format!("0{}", hex_value.to_lowercase()));
209            } else if hex_value.is_empty() {
210                str_arr.push("0".to_string());
211            } else {
212                str_arr.push(hex_value.to_lowercase());
213            }
214        }
215
216        // Add final zeros
217        str_arr.push("0".to_string());
218        str_arr.push("0".to_string());
219
220        // Join and remove dots and dashes
221        let animation_key = str_arr.join("");
222        animation_key.replace(['.', '-'], "")
223    }
224
225    fn get_animation_key(
226        key_bytes: &[u8],
227        page: &Html,
228        row_index: usize,
229        key_bytes_indices: &[usize],
230    ) -> Result<String, Error> {
231        let total_time = 4096.0;
232
233        let row_index_value = (key_bytes[row_index] % 16) as usize;
234
235        let frame_time = key_bytes_indices
236            .iter()
237            .map(|&index| (key_bytes[index] % 16) as f64)
238            .fold(1.0, |acc, val| acc * val);
239
240        let frame_time = js_round(frame_time / 10.0) * 10.0;
241
242        let arr = Self::get_2d_array(key_bytes, page, None)?;
243
244        if row_index_value >= arr.len() {
245            return Err(Error::Parse("Invalid row index".into()));
246        }
247
248        let frame_row = &arr[row_index_value];
249
250        let target_time = frame_time / total_time;
251        let animation_key = Self::animate(frame_row, target_time);
252
253        Ok(animation_key)
254    }
255
256    pub fn generate_transaction_id(&self, method: &str, path: &str) -> Result<String, Error> {
257        let time_now = SystemTime::now()
258            .duration_since(UNIX_EPOCH)
259            .unwrap()
260            .as_secs()
261            .saturating_sub(1682924400) as u32;
262
263        let time_now_bytes = [
264            (time_now & 0xFF) as u8,
265            ((time_now >> 8) & 0xFF) as u8,
266            ((time_now >> 16) & 0xFF) as u8,
267            ((time_now >> 24) & 0xFF) as u8,
268        ];
269
270        let hash_input = format!(
271            "{}!{}!{}{}{}",
272            method, path, time_now, self.default_keyword, self.animation_key
273        );
274
275        let mut hasher = Sha256::new();
276        hasher.update(hash_input.as_bytes());
277        let hash_result = hasher.finalize();
278
279        // Convert the first 16 bytes of the hash to a vector
280        let hash_bytes: Vec<u8> = hash_result[..16].to_vec();
281
282        // Generate a random number between 0 and 255
283        let random_num = rand::random::<u8>();
284
285        // Combine all bytes and XOR with random number
286        let mut bytes_arr =
287            Vec::with_capacity(self.key_bytes.len() + time_now_bytes.len() + hash_bytes.len() + 1);
288        bytes_arr.extend_from_slice(&self.key_bytes);
289        bytes_arr.extend_from_slice(&time_now_bytes);
290        bytes_arr.extend_from_slice(&hash_bytes);
291        bytes_arr.push(self.additional_random_number);
292
293        // Create an output array starting with the random number
294        let mut out = vec![random_num];
295
296        // XOR all bytes with the random number
297        out.extend(bytes_arr.iter().map(|&b| b ^ random_num));
298
299        // Base64 encode and remove padding
300        let encoded = base64_encode(&out);
301        let result = encoded.trim_end_matches('=');
302
303        Ok(result.to_string())
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use reqwest::blocking::Client;
311
312    #[test]
313    #[ignore = "Ignoring network-dependent test"]
314    fn test_transaction_id_generation() -> Result<(), Error> {
315        let client = Client::builder()
316            .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36")
317            .build()?;
318
319        let transaction = ClientTransaction::new(&client)?;
320        let transaction_id =
321            transaction.generate_transaction_id("GET", "/i/api/1.1/jot/client_event.json")?;
322
323        // Verify we get a non-empty string
324        assert!(!transaction_id.is_empty());
325
326        Ok(())
327    }
328}