emusks 2.0.18 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +19 -2
- package/src/cycletls.js +2 -1
- package/src/flow.js +69 -5
- package/src/graphql.js +5 -1
- package/src/helpers/index.js +4 -0
- package/src/helpers/jetfuel.js +175 -0
- package/src/helpers/juicebox/chunk-BNv3lrIs.js +1 -0
- package/src/helpers/juicebox/index.js +30 -0
- package/src/helpers/juicebox/juicebox-sdk_bg.wasm +0 -0
- package/src/helpers/juicebox/sdk.js +1 -0
- package/src/helpers/lists.js +6 -2
- package/src/helpers/tweets.js +3 -1
- package/src/helpers/xchat-call-media.js +127 -0
- package/src/helpers/xchat-calls.js +553 -0
- package/src/helpers/xchat-crypto.js +324 -0
- package/src/helpers/xchat-group-calls.js +340 -0
- package/src/helpers/xchat-juicebox.js +41 -0
- package/src/helpers/xchat-queries.js +3 -0
- package/src/helpers/xchat.js +794 -0
- package/src/index.js +2 -0
- package/src/instrumentation.js +124 -0
- package/src/jetfuel.js +92 -0
- package/src/parsers/jetfuel.js +226 -0
- package/src/v1.1.js +0 -11
- package/build/graphql.js +0 -19
- package/build/v1.1.js +0 -28
- package/build/v2.js +0 -28
- package/bun.lock +0 -93
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import getCycleTLS from "./cycletls.js";
|
|
|
4
4
|
import flowLogin from "./flow.js";
|
|
5
5
|
import graphql, { GRAPHQL_ENDPOINTS } from "./graphql.js";
|
|
6
6
|
import initHelpers from "./helpers/index.js";
|
|
7
|
+
import jetfuel from "./jetfuel.js";
|
|
7
8
|
import parseUser from "./parsers/user.js";
|
|
8
9
|
import v1_1 from "./v1.1.js";
|
|
9
10
|
import v2 from "./v2.js";
|
|
@@ -193,4 +194,5 @@ export default class Emusks {
|
|
|
193
194
|
Emusks.prototype.graphql = graphql;
|
|
194
195
|
Emusks.prototype.v1_1 = v1_1;
|
|
195
196
|
Emusks.prototype.v2 = v2;
|
|
197
|
+
Emusks.prototype.jf = jetfuel;
|
|
196
198
|
initHelpers(Emusks.prototype);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
|
|
3
|
+
// thanks claude for writing this
|
|
4
|
+
|
|
5
|
+
const DOC_HTML =
|
|
6
|
+
"<!DOCTYPE html><html><head></head><body><input type='hidden' name='ui_metrics' value=''></body></html>";
|
|
7
|
+
|
|
8
|
+
let innerTextPatched = false;
|
|
9
|
+
|
|
10
|
+
function patchInnerText(document) {
|
|
11
|
+
if (innerTextPatched) return;
|
|
12
|
+
let proto = Object.getPrototypeOf(document.createElement("div"));
|
|
13
|
+
while (proto && !Object.getOwnPropertyDescriptor(proto, "innerText")) {
|
|
14
|
+
proto = Object.getPrototypeOf(proto);
|
|
15
|
+
}
|
|
16
|
+
if (proto) {
|
|
17
|
+
Object.defineProperty(proto, "innerText", {
|
|
18
|
+
configurable: true,
|
|
19
|
+
get() {
|
|
20
|
+
return this.textContent;
|
|
21
|
+
},
|
|
22
|
+
set(v) {
|
|
23
|
+
this.textContent = String(v);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
innerTextPatched = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function solveJsInstrumentation(challengeJs) {
|
|
31
|
+
const { document } = parseHTML(DOC_HTML);
|
|
32
|
+
patchInnerText(document);
|
|
33
|
+
|
|
34
|
+
if (typeof document.getElementsByName !== "function") {
|
|
35
|
+
document.getElementsByName = (name) => document.querySelectorAll(`[name="${name}"]`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
Object.defineProperty(document, "readyState", { configurable: true, get: () => "complete" });
|
|
40
|
+
} catch {}
|
|
41
|
+
|
|
42
|
+
document.addEventListener = (type, fn) => {
|
|
43
|
+
if (type === "DOMContentLoaded") {
|
|
44
|
+
try {
|
|
45
|
+
fn();
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
document.removeEventListener = () => {};
|
|
50
|
+
|
|
51
|
+
const sandboxWindow = {
|
|
52
|
+
setTimeout: (fn) => {
|
|
53
|
+
try {
|
|
54
|
+
fn();
|
|
55
|
+
} catch {}
|
|
56
|
+
},
|
|
57
|
+
clearTimeout: () => {},
|
|
58
|
+
addEventListener: (type, fn) => {
|
|
59
|
+
if (type === "DOMContentLoaded" || type === "load") {
|
|
60
|
+
try {
|
|
61
|
+
fn();
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
removeEventListener: () => {},
|
|
66
|
+
document,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const navigator = {
|
|
70
|
+
userAgent:
|
|
71
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
|
|
72
|
+
language: "en-US",
|
|
73
|
+
languages: ["en-US", "en"],
|
|
74
|
+
platform: "MacIntel",
|
|
75
|
+
vendor: "Google Inc.",
|
|
76
|
+
appName: "Netscape",
|
|
77
|
+
cookieEnabled: true,
|
|
78
|
+
};
|
|
79
|
+
const screen = {
|
|
80
|
+
width: 1512,
|
|
81
|
+
height: 982,
|
|
82
|
+
availWidth: 1512,
|
|
83
|
+
availHeight: 944,
|
|
84
|
+
colorDepth: 30,
|
|
85
|
+
pixelDepth: 30,
|
|
86
|
+
};
|
|
87
|
+
const location = {
|
|
88
|
+
href: "https://x.com/",
|
|
89
|
+
protocol: "https:",
|
|
90
|
+
host: "x.com",
|
|
91
|
+
hostname: "x.com",
|
|
92
|
+
pathname: "/",
|
|
93
|
+
};
|
|
94
|
+
sandboxWindow.navigator = navigator;
|
|
95
|
+
sandboxWindow.screen = screen;
|
|
96
|
+
sandboxWindow.location = location;
|
|
97
|
+
|
|
98
|
+
const runner = new Function(
|
|
99
|
+
"document",
|
|
100
|
+
"window",
|
|
101
|
+
"navigator",
|
|
102
|
+
"screen",
|
|
103
|
+
"location",
|
|
104
|
+
"self",
|
|
105
|
+
"globalThis",
|
|
106
|
+
challengeJs,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
runner(document, sandboxWindow, navigator, screen, location, sandboxWindow, sandboxWindow);
|
|
110
|
+
|
|
111
|
+
const input = document.getElementsByName("ui_metrics")[0];
|
|
112
|
+
const value = input?.value || null;
|
|
113
|
+
|
|
114
|
+
if (!value) {
|
|
115
|
+
throw new Error("js_instrumentation challenge produced no ui_metrics value");
|
|
116
|
+
}
|
|
117
|
+
if (value.startsWith("exception ")) {
|
|
118
|
+
throw new Error(`js_instrumentation challenge threw: ${value}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default solveJsInstrumentation;
|
package/src/jetfuel.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import getCycleTLS from "./cycletls.js";
|
|
2
|
+
import { decodeJetfuel } from "./parsers/jetfuel.js";
|
|
3
|
+
|
|
4
|
+
const JFAPI_BASE = "https://x.com/i/jfapi";
|
|
5
|
+
const JF_MOUNT = "https://x.com/i/jf";
|
|
6
|
+
const JF_ORIGIN = "https://jf.x.com";
|
|
7
|
+
|
|
8
|
+
export default async function jetfuel(route, opts = {}) {
|
|
9
|
+
if (!this.auth) throw new Error("must be logged in before calling jetfuel");
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
params,
|
|
13
|
+
method = "GET",
|
|
14
|
+
body,
|
|
15
|
+
theme = "business", // fuck it why not
|
|
16
|
+
jfVersion = "JP-5",
|
|
17
|
+
headers,
|
|
18
|
+
referer,
|
|
19
|
+
origin = "jfapi",
|
|
20
|
+
} = opts;
|
|
21
|
+
|
|
22
|
+
const cleanRoute = String(route).replace(/^\/+/, "");
|
|
23
|
+
let finalUrl = `${origin === "jf" ? JF_ORIGIN : JFAPI_BASE}/${cleanRoute}`;
|
|
24
|
+
|
|
25
|
+
if (params && Object.keys(params).length) {
|
|
26
|
+
const searchParams = new URLSearchParams(params);
|
|
27
|
+
const separator = finalUrl.includes("?") ? "&" : "?";
|
|
28
|
+
finalUrl = `${finalUrl}${separator}${searchParams.toString()}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const url = new URL(finalUrl);
|
|
32
|
+
const pathname = url.pathname;
|
|
33
|
+
const routePath = cleanRoute.split("?")[0];
|
|
34
|
+
|
|
35
|
+
const requestHeaders = {
|
|
36
|
+
accept: "*/*",
|
|
37
|
+
"accept-language": "en",
|
|
38
|
+
authorization: `Bearer ${this.auth.client.bearer}`,
|
|
39
|
+
"x-csrf-token": this.auth.csrfToken,
|
|
40
|
+
"x-twitter-active-user": "yes",
|
|
41
|
+
"x-twitter-auth-type": "OAuth2Session",
|
|
42
|
+
"x-twitter-client-language": "en",
|
|
43
|
+
"x-jf-client-theme": theme,
|
|
44
|
+
"x-jf-v": jfVersion,
|
|
45
|
+
priority: "u=1, i",
|
|
46
|
+
"sec-ch-ua": '"Not(A:Brand";v="8", "Chromium";v="144"',
|
|
47
|
+
"sec-ch-ua-mobile": "?0",
|
|
48
|
+
"sec-ch-ua-platform": '"macOS"',
|
|
49
|
+
"sec-fetch-dest": "empty",
|
|
50
|
+
"sec-fetch-mode": "cors",
|
|
51
|
+
"sec-fetch-site": "same-origin",
|
|
52
|
+
"sec-gpc": "1",
|
|
53
|
+
cookie:
|
|
54
|
+
this.auth.client.headers.cookie + (this.elevatedCookies ? `; ${this.elevatedCookies}` : ""),
|
|
55
|
+
...headers,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (this.auth.generateTransactionId) {
|
|
59
|
+
requestHeaders["x-client-transaction-id"] = await this.auth.generateTransactionId(
|
|
60
|
+
method.toUpperCase(),
|
|
61
|
+
pathname,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const cycleTLS = await getCycleTLS();
|
|
66
|
+
const res = await cycleTLS(
|
|
67
|
+
finalUrl,
|
|
68
|
+
{
|
|
69
|
+
headers: requestHeaders,
|
|
70
|
+
userAgent: this.auth.client.fingerprints.userAgent,
|
|
71
|
+
ja3: this.auth.client.fingerprints.ja3,
|
|
72
|
+
ja4r: this.auth.client.fingerprints.ja4r,
|
|
73
|
+
body: body || undefined,
|
|
74
|
+
proxy: this.proxy || undefined,
|
|
75
|
+
referrer: referer || `${JF_MOUNT}/${routePath}`,
|
|
76
|
+
responseType: "arraybuffer",
|
|
77
|
+
},
|
|
78
|
+
method,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
82
|
+
const contentType = res.headers["content-type"] || res.headers["Content-Type"] || null;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
status: res.status,
|
|
86
|
+
headers: res.headers,
|
|
87
|
+
contentType: Array.isArray(contentType) ? contentType[0] : contentType,
|
|
88
|
+
buffer,
|
|
89
|
+
text: () => buffer.toString("utf8"),
|
|
90
|
+
decode: () => decodeJetfuel(buffer),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
2
|
+
|
|
3
|
+
function toBytes(input) {
|
|
4
|
+
if (input == null) return new Uint8Array(0);
|
|
5
|
+
if (input instanceof Uint8Array) return input;
|
|
6
|
+
if (input instanceof ArrayBuffer) return new Uint8Array(input);
|
|
7
|
+
if (ArrayBuffer.isView(input)) {
|
|
8
|
+
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
9
|
+
}
|
|
10
|
+
if (typeof input === "string") return new Uint8Array(Buffer.from(input, "latin1"));
|
|
11
|
+
throw new TypeError("decodeJetfuel expects a Buffer, ArrayBuffer, TypedArray or binary string");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function u16le(b, i) {
|
|
15
|
+
return b[i] | (b[i + 1] << 8);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function reversedHex(b, i, n) {
|
|
19
|
+
let out = "";
|
|
20
|
+
for (let k = n - 1; k >= 0; k--) {
|
|
21
|
+
out += b[i + k].toString(16).padStart(2, "0");
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function looksLikeString(b, start, len, n) {
|
|
27
|
+
if (len < 1 || start + len > n) return false;
|
|
28
|
+
let hasAlpha = false;
|
|
29
|
+
for (let k = start; k < start + len; k++) {
|
|
30
|
+
const c = b[k];
|
|
31
|
+
if (c < 0x20 || c >= 0x7f) return false;
|
|
32
|
+
if ((c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a)) hasAlpha = true;
|
|
33
|
+
}
|
|
34
|
+
return hasAlpha;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function decodeJetfuel(input) {
|
|
38
|
+
const b = toBytes(input);
|
|
39
|
+
const n = b.length;
|
|
40
|
+
|
|
41
|
+
const frame =
|
|
42
|
+
n >= 6
|
|
43
|
+
? {
|
|
44
|
+
payloadLen: u16le(b, 0),
|
|
45
|
+
count: b[5],
|
|
46
|
+
footer: n >= 3 ? reversedHex(b, n - 3, 3).match(/../g).reverse().join("") : null,
|
|
47
|
+
}
|
|
48
|
+
: { payloadLen: null, count: null, footer: null };
|
|
49
|
+
|
|
50
|
+
const tokens = [];
|
|
51
|
+
const strings = [];
|
|
52
|
+
const atoms = [];
|
|
53
|
+
const lists = [];
|
|
54
|
+
const nodes = [];
|
|
55
|
+
|
|
56
|
+
let i = n > 6 ? 6 : 0;
|
|
57
|
+
const end = n > 3 ? n - 3 : n;
|
|
58
|
+
|
|
59
|
+
while (i < end) {
|
|
60
|
+
const byte = b[i];
|
|
61
|
+
|
|
62
|
+
// length-prefixed UTF-8 string: <len:u8> <bytes>
|
|
63
|
+
if (byte >= 1 && byte <= 0xff && looksLikeString(b, i + 1, byte, n)) {
|
|
64
|
+
const value = decoder.decode(b.subarray(i + 1, i + 1 + byte));
|
|
65
|
+
const tok = { type: "string", value, offset: i };
|
|
66
|
+
tokens.push(tok);
|
|
67
|
+
strings.push(value);
|
|
68
|
+
i += 1 + byte;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// node intro: 0x11 then [typeString, keyString]
|
|
73
|
+
if (byte === 0x11) {
|
|
74
|
+
tokens.push({ type: "node", offset: i });
|
|
75
|
+
nodes.push({ type: null, key: null, offset: i });
|
|
76
|
+
i += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// atom ref: 02 <field:u8> 00 00 00 <hash:u32>
|
|
81
|
+
if (byte === 0x02 && i + 9 <= n && b[i + 2] === 0 && b[i + 3] === 0 && b[i + 4] === 0) {
|
|
82
|
+
const field = b[i + 1];
|
|
83
|
+
const hash = reversedHex(b, i + 5, 4);
|
|
84
|
+
tokens.push({ type: "atom", field, hash, offset: i });
|
|
85
|
+
atoms.push({ field, hash });
|
|
86
|
+
i += 9;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ref list: 03 <kind:u8> <count:u8> 00 00 00 00 <hash:u32 × count>
|
|
91
|
+
if (byte === 0x03 && i + 7 <= n && b[i + 3] === 0 && b[i + 4] === 0 && b[i + 5] === 0 && b[i + 6] === 0) {
|
|
92
|
+
const kind = b[i + 1];
|
|
93
|
+
const count = b[i + 2];
|
|
94
|
+
const hashes = [];
|
|
95
|
+
let p = i + 7;
|
|
96
|
+
for (let k = 0; k < count && p + 4 <= n; k++) {
|
|
97
|
+
hashes.push(reversedHex(b, p, 4));
|
|
98
|
+
p += 4;
|
|
99
|
+
}
|
|
100
|
+
tokens.push({ type: "list", kind, count, hashes, offset: i });
|
|
101
|
+
lists.push({ kind, count, hashes });
|
|
102
|
+
i = p;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// unknown control bytes: collapse the run
|
|
107
|
+
let j = i;
|
|
108
|
+
while (
|
|
109
|
+
j < end &&
|
|
110
|
+
!(b[j] >= 1 && b[j] <= 0xff && looksLikeString(b, j + 1, b[j], n)) &&
|
|
111
|
+
b[j] !== 0x11 &&
|
|
112
|
+
!(b[j] === 0x02 && j + 9 <= n && b[j + 2] === 0 && b[j + 3] === 0 && b[j + 4] === 0) &&
|
|
113
|
+
!(b[j] === 0x03 && j + 7 <= n && b[j + 3] === 0 && b[j + 4] === 0 && b[j + 5] === 0 && b[j + 6] === 0)
|
|
114
|
+
) {
|
|
115
|
+
j++;
|
|
116
|
+
}
|
|
117
|
+
tokens.push({ type: "raw", hex: bytesToHex(b, i, j - i), offset: i });
|
|
118
|
+
i = j > i ? j : i + 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (let k = 0; k < tokens.length; k++) {
|
|
122
|
+
if (tokens[k].type !== "node") continue;
|
|
123
|
+
const node = nodes.find((nd) => nd.offset === tokens[k].offset);
|
|
124
|
+
if (!node) continue;
|
|
125
|
+
const following = [];
|
|
126
|
+
for (let j = k + 1; j < tokens.length && following.length < 2; j++) {
|
|
127
|
+
if (tokens[j].type === "node") break;
|
|
128
|
+
if (tokens[j].type === "string") following.push(tokens[j].value);
|
|
129
|
+
}
|
|
130
|
+
[node.type = null, node.key = null] = following;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
frame,
|
|
135
|
+
tokens,
|
|
136
|
+
strings,
|
|
137
|
+
nodes,
|
|
138
|
+
atoms,
|
|
139
|
+
lists,
|
|
140
|
+
timelineTokens: extractTimelineTokens(b),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function bytesToHex(b, start, len) {
|
|
145
|
+
let out = "";
|
|
146
|
+
for (let k = 0; k < len; k++) out += b[start + k].toString(16).padStart(2, "0");
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function extractTimelineTokens(input) {
|
|
151
|
+
const b = toBytes(input);
|
|
152
|
+
const text = Buffer.from(b).toString("latin1");
|
|
153
|
+
const out = [];
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
|
|
156
|
+
const re = /VGltZWxpbmU6[A-Za-z0-9+/]+={0,2}/g; // hard coded because why not
|
|
157
|
+
let m;
|
|
158
|
+
while ((m = re.exec(text))) {
|
|
159
|
+
if (seen.has(m[0])) continue;
|
|
160
|
+
try {
|
|
161
|
+
const decoded = Buffer.from(m[0], "base64").toString("latin1");
|
|
162
|
+
if (decoded.startsWith("Timeline:")) {
|
|
163
|
+
seen.add(m[0]);
|
|
164
|
+
out.push(m[0]);
|
|
165
|
+
}
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const ENGAGEMENT_FIELD = 1;
|
|
172
|
+
const COUNTRY_FIELD = 3;
|
|
173
|
+
const PERIOD_FIELD = 6;
|
|
174
|
+
|
|
175
|
+
export function buildTimelineToken({ engagement = "Likes", country = "All", period = "Daily" } = {}) {
|
|
176
|
+
const strField = (id, value) => {
|
|
177
|
+
const valueBytes = Buffer.from(String(value), "utf8");
|
|
178
|
+
const head = Buffer.alloc(7);
|
|
179
|
+
head[0] = 0x0b; // thrift binary: STRING
|
|
180
|
+
head.writeUInt16BE(id, 1);
|
|
181
|
+
head.writeUInt32BE(valueBytes.length, 3);
|
|
182
|
+
return Buffer.concat([head, valueBytes]);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const inner = Buffer.concat([
|
|
186
|
+
strField(ENGAGEMENT_FIELD, engagement),
|
|
187
|
+
strField(COUNTRY_FIELD, country),
|
|
188
|
+
strField(PERIOD_FIELD, period),
|
|
189
|
+
Buffer.from([0x00]),
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
const thrift = Buffer.concat([
|
|
193
|
+
Buffer.from([0x0c, 0x00, 0xca]), // STRUCT, field id 202
|
|
194
|
+
inner,
|
|
195
|
+
Buffer.from([0x00]),
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
return Buffer.concat([Buffer.from("Timeline:", "utf8"), thrift]).toString("base64");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function decodeTimelineToken(token) {
|
|
202
|
+
const raw = Buffer.from(token, "base64");
|
|
203
|
+
const prefix = Buffer.from("Timeline:", "utf8");
|
|
204
|
+
if (!raw.subarray(0, prefix.length).equals(prefix)) {
|
|
205
|
+
throw new Error("not a Timeline: token");
|
|
206
|
+
}
|
|
207
|
+
const b = raw.subarray(prefix.length);
|
|
208
|
+
const fields = {};
|
|
209
|
+
let i = 0;
|
|
210
|
+
if (b[i] === 0x0c) i += 3; // skip outer struct header (STRUCT, id 202)
|
|
211
|
+
while (i < b.length && b[i] !== 0x00) {
|
|
212
|
+
const type = b[i];
|
|
213
|
+
const id = b.readUInt16BE(i + 1);
|
|
214
|
+
i += 3;
|
|
215
|
+
if (type !== 0x0b) break; // only string fields expected
|
|
216
|
+
const len = b.readUInt32BE(i);
|
|
217
|
+
i += 4;
|
|
218
|
+
fields[id] = b.subarray(i, i + len).toString("utf8");
|
|
219
|
+
i += len;
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
engagement: fields[ENGAGEMENT_FIELD] ?? null,
|
|
223
|
+
country: fields[COUNTRY_FIELD] ?? null,
|
|
224
|
+
period: fields[PERIOD_FIELD] ?? null,
|
|
225
|
+
};
|
|
226
|
+
}
|
package/src/v1.1.js
CHANGED
|
@@ -20,12 +20,6 @@ export default async function v1_1(queryName, { params, body, headers } = {}) {
|
|
|
20
20
|
const url = new URL(finalUrl);
|
|
21
21
|
const pathname = url.pathname;
|
|
22
22
|
|
|
23
|
-
// v1.1 endpoints want form-urlencoded bodies. helpers historically passed
|
|
24
|
-
// `body: JSON.stringify(...)`, which twitter silently rejected — the most
|
|
25
|
-
// recent example was friendships/create returning "Cannot find specified
|
|
26
|
-
// user" because user_id wasn't extractable from a JSON body. if the caller
|
|
27
|
-
// hasn't set its own content-type, transparently convert a JSON-string body
|
|
28
|
-
// into form-urlencoded so existing helpers Just Work.
|
|
29
23
|
const callerSetContentType = headers && Object.keys(headers).some(
|
|
30
24
|
(k) => k.toLowerCase() === "content-type",
|
|
31
25
|
);
|
|
@@ -87,10 +81,6 @@ export default async function v1_1(queryName, { params, body, headers } = {}) {
|
|
|
87
81
|
method,
|
|
88
82
|
);
|
|
89
83
|
|
|
90
|
-
// surface twitter errors instead of returning silently. matches the pattern
|
|
91
|
-
// in graphql.js (it checks res.errors after .json()). without this, callers
|
|
92
|
-
// that just `await res.json()` swallow 4xx error bodies — e.g. profile
|
|
93
|
-
// updates over 160 chars used to no-op silently.
|
|
94
84
|
let bodyText;
|
|
95
85
|
try {
|
|
96
86
|
bodyText = await res.text();
|
|
@@ -107,7 +97,6 @@ export default async function v1_1(queryName, { params, body, headers } = {}) {
|
|
|
107
97
|
.join("; ");
|
|
108
98
|
throw new Error(`twitter v1.1 ${queryName} ${res.status}: ${messages}`);
|
|
109
99
|
}
|
|
110
|
-
// re-expose the response so existing helpers keep working
|
|
111
100
|
return {
|
|
112
101
|
status: res.status,
|
|
113
102
|
headers: res.headers,
|
package/build/graphql.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
const URL = `https://raw.githubusercontent.com/fa0311/TwitterInternalAPIDocument/refs/heads/develop/docs/json/API.json`;
|
|
2
|
-
|
|
3
|
-
const json = await fetch(URL).then((r) => r.json());
|
|
4
|
-
const graphql = json.graphql;
|
|
5
|
-
|
|
6
|
-
const transformed = Object.fromEntries(
|
|
7
|
-
Object.entries(graphql).map(([name, data]) => {
|
|
8
|
-
const features = data.features
|
|
9
|
-
? Object.fromEntries(Object.keys(data.features).map((k) => [k, true]))
|
|
10
|
-
: undefined;
|
|
11
|
-
const queryId = data.url.match(/\/graphql\/([^/]+)\//)?.[1];
|
|
12
|
-
return [name, [data.method, data.url, features, queryId]];
|
|
13
|
-
}),
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
await Bun.write(
|
|
17
|
-
`./src/static/graphql.js`,
|
|
18
|
-
`export default ${JSON.stringify(transformed)};`,
|
|
19
|
-
);
|
package/build/v1.1.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
const URL = `https://raw.githubusercontent.com/fa0311/TwitterInternalAPIDocument/refs/heads/develop/docs/json/v1.1.json`;
|
|
2
|
-
|
|
3
|
-
const json = await fetch(URL).then((r) => r.json());
|
|
4
|
-
|
|
5
|
-
const seen = new Set();
|
|
6
|
-
const duplicates = new Set();
|
|
7
|
-
|
|
8
|
-
for (const entry of json) {
|
|
9
|
-
const queryId = entry.queryId.replace(/^\//, "");
|
|
10
|
-
if (seen.has(queryId)) {
|
|
11
|
-
duplicates.add(queryId);
|
|
12
|
-
}
|
|
13
|
-
seen.add(queryId);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const transformed = {};
|
|
17
|
-
|
|
18
|
-
for (const entry of json) {
|
|
19
|
-
const queryId = entry.queryId.replace(/^\//, "");
|
|
20
|
-
const method = entry.dispatch[0];
|
|
21
|
-
const urlTemplate = entry.dispatch[2];
|
|
22
|
-
const url = urlTemplate.replace("{queryId}", queryId);
|
|
23
|
-
|
|
24
|
-
const key = duplicates.has(queryId) ? `${method}:${queryId}` : queryId;
|
|
25
|
-
transformed[key] = [method, url];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
await Bun.write(`./src/static/v1.1.js`, `export default ${JSON.stringify(transformed)};`);
|
package/build/v2.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
const URL = `https://raw.githubusercontent.com/fa0311/TwitterInternalAPIDocument/refs/heads/develop/docs/json/v2.json`;
|
|
2
|
-
|
|
3
|
-
const json = await fetch(URL).then((r) => r.json());
|
|
4
|
-
|
|
5
|
-
const seen = new Set();
|
|
6
|
-
const duplicates = new Set();
|
|
7
|
-
|
|
8
|
-
for (const entry of json) {
|
|
9
|
-
const queryId = entry.queryId.replace(/^\//, "");
|
|
10
|
-
if (seen.has(queryId)) {
|
|
11
|
-
duplicates.add(queryId);
|
|
12
|
-
}
|
|
13
|
-
seen.add(queryId);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const transformed = {};
|
|
17
|
-
|
|
18
|
-
for (const entry of json) {
|
|
19
|
-
const queryId = entry.queryId.replace(/^\//, "");
|
|
20
|
-
const method = entry.dispatch[0];
|
|
21
|
-
const urlTemplate = entry.dispatch[2];
|
|
22
|
-
const url = urlTemplate.replace("{queryId}", queryId);
|
|
23
|
-
|
|
24
|
-
const key = duplicates.has(queryId) ? `${method}:${queryId}` : queryId;
|
|
25
|
-
transformed[key] = [method, url];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
await Bun.write(`./src/static/v2.js`, `export default ${JSON.stringify(transformed)};`);
|
package/bun.lock
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lockfileVersion": 1,
|
|
3
|
-
"configVersion": 1,
|
|
4
|
-
"workspaces": {
|
|
5
|
-
"": {
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"cycletls": "^2.0.5",
|
|
8
|
-
"x-client-transaction-id": "^0.1.9",
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
"packages": {
|
|
13
|
-
"@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],
|
|
14
|
-
|
|
15
|
-
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
|
16
|
-
|
|
17
|
-
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
|
18
|
-
|
|
19
|
-
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
|
20
|
-
|
|
21
|
-
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
|
22
|
-
|
|
23
|
-
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
|
24
|
-
|
|
25
|
-
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
|
26
|
-
|
|
27
|
-
"cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="],
|
|
28
|
-
|
|
29
|
-
"cycletls": ["cycletls@2.0.5", "", { "dependencies": { "@types/node": "^20.14.0", "form-data": "^4.0.0", "ws": "^8.17.0" } }, "sha512-Nud94JBPZu1JTXWJ1GpIoQNR1xxU84WYt7G0dxzwpUe1xAAs6YbihvlQhIhPv9xljjonINJfWPFaeb8Ex1wLhQ=="],
|
|
30
|
-
|
|
31
|
-
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
|
32
|
-
|
|
33
|
-
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
|
34
|
-
|
|
35
|
-
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
|
36
|
-
|
|
37
|
-
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
|
38
|
-
|
|
39
|
-
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
|
40
|
-
|
|
41
|
-
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
|
42
|
-
|
|
43
|
-
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
|
44
|
-
|
|
45
|
-
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
|
46
|
-
|
|
47
|
-
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
|
48
|
-
|
|
49
|
-
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
|
50
|
-
|
|
51
|
-
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
|
52
|
-
|
|
53
|
-
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
|
54
|
-
|
|
55
|
-
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
|
56
|
-
|
|
57
|
-
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
|
58
|
-
|
|
59
|
-
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
|
60
|
-
|
|
61
|
-
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
|
62
|
-
|
|
63
|
-
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
|
64
|
-
|
|
65
|
-
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
|
66
|
-
|
|
67
|
-
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
|
68
|
-
|
|
69
|
-
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
|
|
70
|
-
|
|
71
|
-
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
|
72
|
-
|
|
73
|
-
"linkedom": ["linkedom@0.18.12", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q=="],
|
|
74
|
-
|
|
75
|
-
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
|
76
|
-
|
|
77
|
-
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
|
78
|
-
|
|
79
|
-
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
|
80
|
-
|
|
81
|
-
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
|
82
|
-
|
|
83
|
-
"uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="],
|
|
84
|
-
|
|
85
|
-
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
86
|
-
|
|
87
|
-
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
|
88
|
-
|
|
89
|
-
"x-client-transaction-id": ["x-client-transaction-id@0.1.9", "", { "dependencies": { "linkedom": "^0.18.9" } }, "sha512-CES4zgkJ0wbfFWm0qgdKphthyb+L7lVHymgOY15v6ivcWSx5p9lp5kzAed+BuqJSP7bS0GbQyJ16ONkRthgsUw=="],
|
|
90
|
-
|
|
91
|
-
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
|
92
|
-
}
|
|
93
|
-
}
|