x_client_transaction/
transaction.rs1use 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 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 let on_demand_response = client.get(&on_demand_file_url).send()?;
62 let on_demand_content = on_demand_response.text()?;
63
64 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 for value in &color[..color.len() - 1] {
198 str_arr.push(format!("{:x}", value.round() as i32));
199 }
200
201 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 str_arr.push("0".to_string());
218 str_arr.push("0".to_string());
219
220 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 let hash_bytes: Vec<u8> = hash_result[..16].to_vec();
281
282 let random_num = rand::random::<u8>();
284
285 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 let mut out = vec![random_num];
295
296 out.extend(bytes_arr.iter().map(|&b| b ^ random_num));
298
299 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 assert!(!transaction_id.is_empty());
325
326 Ok(())
327 }
328}