Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mikf
GitHub Repository: mikf/gallery-dl
Path: blob/master/gallery_dl/transaction_id.py
5457 views
1
# -*- coding: utf-8 -*-
2
3
# Copyright 2025 Mike Fährmann
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License version 2 as
7
# published by the Free Software Foundation.
8
9
# Adapted from iSarabjitDhiman/XClientTransaction
10
# https://github.com/iSarabjitDhiman/XClientTransaction
11
12
# References:
13
# https://antibot.blog/posts/1741552025433
14
# https://antibot.blog/posts/1741552092462
15
# https://antibot.blog/posts/1741552163416
16
17
"""Twitter 'x-client-transaction-id' header generation"""
18
19
import math
20
import time
21
import random
22
import hashlib
23
import binascii
24
import itertools
25
from . import text, util
26
from .cache import cache
27
28
29
class ClientTransaction():
30
__slots__ = ("key_bytes", "animation_key")
31
32
def __getstate__(self):
33
return (self.key_bytes, self.animation_key)
34
35
def __setstate__(self, state):
36
self.key_bytes, self.animation_key = state
37
38
def initialize(self, extractor, homepage=None):
39
if homepage is None:
40
homepage = extractor.request("https://x.com/").text
41
42
key = self._extract_verification_key(homepage)
43
if not key:
44
extractor.log.error(
45
"Failed to extract 'twitter-site-verification' key")
46
47
ondemand_s = text.extr(homepage, '"ondemand.s":"', '"')
48
indices = self._extract_indices(ondemand_s, extractor)
49
if not indices:
50
extractor.log.error("Failed to extract KEY_BYTE indices")
51
52
frames = self._extract_frames(homepage)
53
if not frames:
54
extractor.log.error("Failed to extract animation frame data")
55
56
self.key_bytes = key_bytes = binascii.a2b_base64(key)
57
self.animation_key = self._calculate_animation_key(
58
frames, indices[0], key_bytes, indices[1:])
59
60
def _extract_verification_key(self, homepage):
61
pos = homepage.find('name="twitter-site-verification"')
62
beg = homepage.rfind("<", 0, pos)
63
end = homepage.find(">", pos)
64
return text.extr(homepage[beg:end], 'content="', '"')
65
66
@cache(maxage=36500*86400, keyarg=1)
67
def _extract_indices(self, ondemand_s, extractor):
68
url = (f"https://abs.twimg.com/responsive-web/client-web"
69
f"/ondemand.s.{ondemand_s}a.js")
70
page = extractor.request(url).text
71
pattern = util.re_compile(r"\(\w\[(\d\d?)\],\s*16\)")
72
return [int(i) for i in pattern.findall(page)]
73
74
def _extract_frames(self, homepage):
75
return list(text.extract_iter(
76
homepage, 'id="loading-x-anim-', "</svg>"))
77
78
def _calculate_animation_key(self, frames, row_index, key_bytes,
79
key_bytes_indices, total_time=4096):
80
frame = frames[key_bytes[5] % 4]
81
array = self._generate_2d_array(frame)
82
frame_row = array[key_bytes[row_index] % 16]
83
84
frame_time = 1
85
for index in key_bytes_indices:
86
frame_time *= key_bytes[index] % 16
87
frame_time = round_js(frame_time / 10) * 10
88
target_time = frame_time / total_time
89
90
return self.animate(frame_row, target_time)
91
92
def _generate_2d_array(self, frame):
93
split = util.re_compile(r"[^\d]+").split
94
return [
95
[int(x) for x in split(path) if x]
96
for path in text.extr(
97
frame, '</path><path d="', '"')[9:].split("C")
98
]
99
100
def animate(self, frames, target_time):
101
curve = [scale(float(frame), is_odd(index), 1.0, False)
102
for index, frame in enumerate(frames[7:])]
103
cubic = cubic_value(curve, target_time)
104
105
color_a = (float(frames[0]), float(frames[1]), float(frames[2]))
106
color_b = (float(frames[3]), float(frames[4]), float(frames[5]))
107
color = interpolate_list(cubic, color_a, color_b)
108
color = [0.0 if c <= 0.0 else 255.0 if c >= 255.0 else c
109
for c in color]
110
111
rotation_a = 0.0
112
rotation_b = scale(float(frames[6]), 60.0, 360.0, True)
113
rotation = interpolate_value(cubic, rotation_a, rotation_b)
114
matrix = rotation_matrix_2d(rotation)
115
116
result = (
117
hex(round(color[0]))[2:],
118
hex(round(color[1]))[2:],
119
hex(round(color[2]))[2:],
120
float_to_hex(abs(round(matrix[0], 2))),
121
float_to_hex(abs(round(matrix[1], 2))),
122
float_to_hex(abs(round(matrix[2], 2))),
123
float_to_hex(abs(round(matrix[3], 2))),
124
"00",
125
)
126
return "".join(result).replace(".", "").replace("-", "")
127
128
def generate_transaction_id(self, method, path,
129
keyword="obfiowerehiring", rndnum=3):
130
bytes_key = self.key_bytes
131
132
nowf = time.time()
133
nowi = int(nowf)
134
now = nowi - 1682924400
135
bytes_time = (
136
(now ) & 0xFF, # noqa: E202
137
(now >> 8) & 0xFF, # noqa: E222
138
(now >> 16) & 0xFF,
139
(now >> 24) & 0xFF,
140
)
141
142
payload = f"{method}!{path}!{now}{keyword}{self.animation_key}"
143
bytes_hash = hashlib.sha256(payload.encode()).digest()[:16]
144
145
num = (random.randrange(16) << 4) + int((nowf - nowi) * 16.0)
146
result = bytes(
147
byte ^ num
148
for byte in itertools.chain(
149
(0,), bytes_key, bytes_time, bytes_hash, (rndnum,))
150
)
151
return binascii.b2a_base64(result).rstrip(b"=\n")
152
153
154
# Cubic Curve
155
156
def cubic_value(curve, t):
157
if t <= 0.0:
158
if curve[0] > 0.0:
159
value = curve[1] / curve[0]
160
elif curve[1] == 0.0 and curve[2] > 0.0:
161
value = curve[3] / curve[2]
162
else:
163
value = 0.0
164
return value * t
165
166
if t >= 1.0:
167
if curve[2] < 1.0:
168
value = (curve[3] - 1.0) / (curve[2] - 1.0)
169
elif curve[2] == 1.0 and curve[0] < 1.0:
170
value = (curve[1] - 1.0) / (curve[0] - 1.0)
171
else:
172
value = 0.0
173
return 1.0 + value * (t - 1.0)
174
175
start = 0.0
176
end = 1.0
177
while start < end:
178
mid = (start + end) / 2.0
179
est = cubic_calculate(curve[0], curve[2], mid)
180
if abs(t - est) < 0.00001:
181
return cubic_calculate(curve[1], curve[3], mid)
182
if est < t:
183
start = mid
184
else:
185
end = mid
186
return cubic_calculate(curve[1], curve[3], mid)
187
188
189
def cubic_calculate(a, b, m):
190
m1 = 1.0 - m
191
return 3.0*a*m1*m1*m + 3.0*b*m1*m*m + m*m*m
192
193
194
# Interpolation
195
196
def interpolate_list(x, a, b):
197
return [
198
interpolate_value(x, a[i], b[i])
199
for i in range(len(a))
200
]
201
202
203
def interpolate_value(x, a, b):
204
if isinstance(a, bool):
205
return a if x <= 0.5 else b
206
return a * (1.0 - x) + b * x
207
208
209
# Rotation
210
211
def rotation_matrix_2d(deg):
212
rad = math.radians(deg)
213
cos = math.cos(rad)
214
sin = math.sin(rad)
215
return [cos, -sin, sin, cos]
216
217
218
# Utilities
219
220
def float_to_hex(numf):
221
numi = int(numf)
222
223
fraction = numf - numi
224
if not fraction:
225
return hex(numi)[2:]
226
227
result = ["."]
228
while fraction > 0.0:
229
fraction *= 16.0
230
integer = int(fraction)
231
fraction -= integer
232
result.append(chr(integer + 87) if integer > 9 else str(integer))
233
return hex(numi)[2:] + "".join(result)
234
235
236
def is_odd(num):
237
return -1.0 if num % 2 else 0.0
238
239
240
def round_js(num):
241
floor = math.floor(num)
242
return floor if (num - floor) < 0.5 else math.ceil(num)
243
244
245
def scale(value, value_min, value_max, rounding):
246
result = value * (value_max-value_min) / 255.0 + value_min
247
return math.floor(result) if rounding else round(result, 2)
248
249