Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Convert X (Twitter) tweets, threads, and articles to Markdown with YAML front matter via reverse-engineered API.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/http.ts
1import { buildCookieHeader } from "./cookies.js";23let cachedHomeHtml: { userAgent: string; html: string } | null = null;45export async function fetchText(url: string, init?: RequestInit): Promise<string> {6const response = await fetch(url, init);7const text = await response.text();8if (!response.ok) {9throw new Error(`Request failed (${response.status}) for ${url}: ${text.slice(0, 200)}`);10}11return text;12}1314export async function fetchHomeHtml(userAgent: string): Promise<string> {15if (cachedHomeHtml?.userAgent === userAgent) {16return cachedHomeHtml.html;17}18const html = await fetchText("https://x.com", {19headers: {20"user-agent": userAgent,21},22});23cachedHomeHtml = { userAgent, html };24return html;25}2627export function parseStringList(raw: string | undefined): string[] {28if (!raw) return [];29return raw30.split(",")31.map((item) => item.trim())32.filter(Boolean)33.map((item) => item.replace(/^\"|\"$/g, ""));34}3536export function resolveFeatureValue(html: string, key: string): boolean | undefined {37const keyPattern = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");38const unescaped = new RegExp(`"${keyPattern}"\\s*:\\s*\\{"value"\\s*:\\s*(true|false)`);39const escaped = new RegExp(`\\\\"${keyPattern}\\\\"\\s*:\\s*\\\\{\\\\"value\\\\"\\s*:\\s*(true|false)`);40const match = html.match(unescaped) ?? html.match(escaped);41if (!match) return undefined;42return match[1] === "true";43}4445export function buildFeatureMap(46html: string,47keys: string[],48defaults?: Record<string, boolean>49): Record<string, boolean> {50const features: Record<string, boolean> = {};51for (const key of keys) {52const value = resolveFeatureValue(html, key);53if (value !== undefined) {54features[key] = value;55} else if (defaults && Object.prototype.hasOwnProperty.call(defaults, key)) {56features[key] = defaults[key] ?? true;57} else {58features[key] = true;59}60}61if (!Object.prototype.hasOwnProperty.call(features, "responsive_web_graphql_exclude_directive_enabled")) {62features.responsive_web_graphql_exclude_directive_enabled = true;63}64return features;65}6667export function buildFieldToggleMap(keys: string[]): Record<string, boolean> {68const toggles: Record<string, boolean> = {};69for (const key of keys) {70toggles[key] = true;71}72return toggles;73}7475export function buildTweetFieldToggleMap(keys: string[]): Record<string, boolean> {76const toggles: Record<string, boolean> = {};77for (const key of keys) {78if (key === "withGrokAnalyze" || key === "withDisallowedReplyControls") {79toggles[key] = false;80} else {81toggles[key] = true;82}83}84return toggles;85}8687export function buildRequestHeaders(88cookieMap: Record<string, string>,89userAgent: string,90bearerToken: string91): Record<string, string> {92const headers: Record<string, string> = {93authorization: bearerToken,94"user-agent": userAgent,95accept: "application/json",96"x-twitter-active-user": "yes",97"x-twitter-client-language": "en",98"accept-language": "en",99};100101if (cookieMap.auth_token) {102headers["x-twitter-auth-type"] = "OAuth2Session";103}104105const cookieHeader = buildCookieHeader(cookieMap);106if (cookieHeader) {107headers.cookie = cookieHeader;108}109if (cookieMap.ct0) {110headers["x-csrf-token"] = cookieMap.ct0;111}112if (process.env.X_CLIENT_TRANSACTION_ID?.trim()) {113headers["x-client-transaction-id"] = process.env.X_CLIENT_TRANSACTION_ID.trim();114}115116return headers;117}118