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/graphql.ts
1import {2DEFAULT_BEARER_TOKEN,3DEFAULT_USER_AGENT,4FALLBACK_FEATURE_SWITCHES,5FALLBACK_FIELD_TOGGLES,6FALLBACK_QUERY_ID,7FALLBACK_TWEET_DETAIL_FEATURE_DEFAULTS,8FALLBACK_TWEET_DETAIL_FEATURE_SWITCHES,9FALLBACK_TWEET_DETAIL_FIELD_TOGGLES,10FALLBACK_TWEET_DETAIL_QUERY_ID,11FALLBACK_TWEET_FEATURE_SWITCHES,12FALLBACK_TWEET_FIELD_TOGGLES,13FALLBACK_TWEET_QUERY_ID,14} from "./constants.js";15import {16buildFeatureMap,17buildFieldToggleMap,18buildRequestHeaders,19buildTweetFieldToggleMap,20fetchHomeHtml,21fetchText,22parseStringList,23} from "./http.js";24import type { ArticleQueryInfo } from "./types.js";2526function isNonEmptyObject(value: unknown): value is Record<string, unknown> {27return Boolean(value && typeof value === "object" && Object.keys(value as Record<string, unknown>).length > 0);28}2930function unwrapTweetResult(result: any): any {31if (!result) return null;32if (result.__typename === "TweetWithVisibilityResults" && result.tweet) {33return result.tweet;34}35return result;36}3738function extractArticleFromTweet(payload: unknown): unknown {39const root = (payload as { data?: any }).data ?? payload;40const result = root?.tweetResult?.result ?? root?.tweet_result?.result ?? root?.tweet_result;41const tweet = unwrapTweetResult(result);42const legacy = tweet?.legacy ?? {};43const article = legacy?.article ?? tweet?.article;44return (45article?.article_results?.result ??46legacy?.article_results?.result ??47tweet?.article_results?.result ??48null49);50}5152function extractTweetFromPayload(payload: unknown): unknown {53const root = (payload as { data?: any }).data ?? payload;54const result = root?.tweetResult?.result ?? root?.tweet_result?.result ?? root?.tweet_result;55return unwrapTweetResult(result);56}5758function extractArticleFromEntity(payload: unknown): unknown {59const root = (payload as { data?: any }).data ?? payload;60return (61root?.article_result_by_rest_id?.result ??62root?.article_result_by_rest_id ??63root?.article_entity_result?.result ??64null65);66}6768async function resolveArticleQueryInfo(userAgent: string): Promise<ArticleQueryInfo> {69const html = await fetchHomeHtml(userAgent);7071const bundleMatch = html.match(/"bundle\\.TwitterArticles":"([a-z0-9]+)"/);72if (!bundleMatch) {73return {74queryId: FALLBACK_QUERY_ID,75featureSwitches: FALLBACK_FEATURE_SWITCHES,76fieldToggles: FALLBACK_FIELD_TOGGLES,77html,78};79}8081const bundleHash = bundleMatch[1];82const chunkUrl = `https://abs.twimg.com/responsive-web/client-web/bundle.TwitterArticles.${bundleHash}a.js`;83const chunk = await fetchText(chunkUrl, {84headers: {85"user-agent": userAgent,86},87});8889const queryIdMatch = chunk.match(/queryId:\"([^\"]+)\",operationName:\"ArticleEntityResultByRestId\"/);90const featureMatch = chunk.match(91/operationName:\"ArticleEntityResultByRestId\"[\s\S]*?featureSwitches:\[(.*?)\]/92);93const fieldToggleMatch = chunk.match(94/operationName:\"ArticleEntityResultByRestId\"[\s\S]*?fieldToggles:\[(.*?)\]/95);9697const featureSwitches = parseStringList(featureMatch?.[1]);98const fieldToggles = parseStringList(fieldToggleMatch?.[1]);99100return {101queryId: queryIdMatch?.[1] ?? FALLBACK_QUERY_ID,102featureSwitches: featureSwitches.length > 0 ? featureSwitches : FALLBACK_FEATURE_SWITCHES,103fieldToggles: fieldToggles.length > 0 ? fieldToggles : FALLBACK_FIELD_TOGGLES,104html,105};106}107108function resolveMainChunkHash(html: string): string | null {109const match = html.match(/main\\.([a-z0-9]+)\\.js/);110return match?.[1] ?? null;111}112113function resolveApiChunkHash(html: string): string | null {114const match = html.match(/api:\"([a-zA-Z0-9_-]+)\"/);115return match?.[1] ?? null;116}117118async function resolveTweetDetailQueryInfo(userAgent: string): Promise<ArticleQueryInfo> {119const html = await fetchHomeHtml(userAgent);120const apiHash = resolveApiChunkHash(html);121if (!apiHash) {122return {123queryId: FALLBACK_TWEET_DETAIL_QUERY_ID,124featureSwitches: FALLBACK_TWEET_DETAIL_FEATURE_SWITCHES,125fieldToggles: FALLBACK_TWEET_DETAIL_FIELD_TOGGLES,126html,127};128}129130const chunkUrl = `https://abs.twimg.com/responsive-web/client-web/api.${apiHash}a.js`;131const chunk = await fetchText(chunkUrl, {132headers: {133"user-agent": userAgent,134},135});136137const queryIdMatch = chunk.match(/queryId:\"([^\"]+)\",operationName:\"TweetDetail\"/);138const featureMatch = chunk.match(139/operationName:\"TweetDetail\"[\s\S]*?featureSwitches:\[(.*?)\]/140);141const fieldToggleMatch = chunk.match(142/operationName:\"TweetDetail\"[\s\S]*?fieldToggles:\[(.*?)\]/143);144145const featureSwitches = parseStringList(featureMatch?.[1]);146const fieldToggles = parseStringList(fieldToggleMatch?.[1]);147148return {149queryId: queryIdMatch?.[1] ?? FALLBACK_TWEET_DETAIL_QUERY_ID,150featureSwitches: featureSwitches.length > 0 ? featureSwitches : FALLBACK_TWEET_DETAIL_FEATURE_SWITCHES,151fieldToggles: fieldToggles.length > 0 ? fieldToggles : FALLBACK_TWEET_DETAIL_FIELD_TOGGLES,152html,153};154}155156function buildTweetDetailFieldToggleMap(keys: string[]): Record<string, boolean> {157const toggles = buildFieldToggleMap(keys);158if (Object.prototype.hasOwnProperty.call(toggles, "withArticlePlainText")) {159toggles.withArticlePlainText = false;160}161if (Object.prototype.hasOwnProperty.call(toggles, "withGrokAnalyze")) {162toggles.withGrokAnalyze = false;163}164if (Object.prototype.hasOwnProperty.call(toggles, "withDisallowedReplyControls")) {165toggles.withDisallowedReplyControls = false;166}167return toggles;168}169170async function resolveTweetQueryInfo(userAgent: string): Promise<ArticleQueryInfo> {171const html = await fetchHomeHtml(userAgent);172const mainHash = resolveMainChunkHash(html);173if (!mainHash) {174return {175queryId: FALLBACK_TWEET_QUERY_ID,176featureSwitches: FALLBACK_TWEET_FEATURE_SWITCHES,177fieldToggles: FALLBACK_TWEET_FIELD_TOGGLES,178html,179};180}181182const chunkUrl = `https://abs.twimg.com/responsive-web/client-web/main.${mainHash}.js`;183const chunk = await fetchText(chunkUrl, {184headers: {185"user-agent": userAgent,186},187});188189const queryIdMatch = chunk.match(/queryId:\"([^\"]+)\",operationName:\"TweetResultByRestId\"/);190const featureMatch = chunk.match(191/operationName:\"TweetResultByRestId\"[\s\S]*?featureSwitches:\[(.*?)\]/192);193const fieldToggleMatch = chunk.match(194/operationName:\"TweetResultByRestId\"[\s\S]*?fieldToggles:\[(.*?)\]/195);196197const featureSwitches = parseStringList(featureMatch?.[1]);198const fieldToggles = parseStringList(fieldToggleMatch?.[1]);199200return {201queryId: queryIdMatch?.[1] ?? FALLBACK_TWEET_QUERY_ID,202featureSwitches: featureSwitches.length > 0 ? featureSwitches : FALLBACK_TWEET_FEATURE_SWITCHES,203fieldToggles: fieldToggles.length > 0 ? fieldToggles : FALLBACK_TWEET_FIELD_TOGGLES,204html,205};206}207208async function fetchTweetResult(209tweetId: string,210cookieMap: Record<string, string>,211userAgent: string,212bearerToken: string213): Promise<unknown> {214const queryInfo = await resolveTweetQueryInfo(userAgent);215const features = buildFeatureMap(queryInfo.html, queryInfo.featureSwitches);216const fieldToggles = buildTweetFieldToggleMap(queryInfo.fieldToggles);217218const url = new URL(`https://x.com/i/api/graphql/${queryInfo.queryId}/TweetResultByRestId`);219url.searchParams.set(220"variables",221JSON.stringify({222tweetId,223withCommunity: false,224includePromotedContent: false,225withVoice: true,226})227);228if (Object.keys(features).length > 0) {229url.searchParams.set("features", JSON.stringify(features));230}231if (Object.keys(fieldToggles).length > 0) {232url.searchParams.set("fieldToggles", JSON.stringify(fieldToggles));233}234235const response = await fetch(url.toString(), {236headers: buildRequestHeaders(cookieMap, userAgent, bearerToken),237});238239const text = await response.text();240if (!response.ok) {241throw new Error(`X API error (${response.status}): ${text.slice(0, 400)}`);242}243244try {245return JSON.parse(text);246} catch (error) {247throw new Error(`Failed to parse response JSON: ${error instanceof Error ? error.message : String(error)}`);248}249}250251export async function fetchTweetDetail(252tweetId: string,253cookieMap: Record<string, string>,254cursor?: string255): Promise<unknown> {256const userAgent = process.env.X_USER_AGENT?.trim() || DEFAULT_USER_AGENT;257const bearerToken = process.env.X_BEARER_TOKEN?.trim() || DEFAULT_BEARER_TOKEN;258const queryInfo = await resolveTweetDetailQueryInfo(userAgent);259const features = buildFeatureMap(260queryInfo.html,261queryInfo.featureSwitches,262FALLBACK_TWEET_DETAIL_FEATURE_DEFAULTS263);264const fieldToggles = buildTweetDetailFieldToggleMap(queryInfo.fieldToggles);265266const url = new URL(`https://x.com/i/api/graphql/${queryInfo.queryId}/TweetDetail`);267url.searchParams.set(268"variables",269JSON.stringify({270focalTweetId: tweetId,271cursor,272referrer: cursor ? "tweet" : undefined,273with_rux_injections: false,274includePromotedContent: true,275withCommunity: true,276withQuickPromoteEligibilityTweetFields: true,277withBirdwatchNotes: true,278withVoice: true,279withV2Timeline: true,280withDownvotePerspective: false,281withReactionsMetadata: false,282withReactionsPerspective: false,283withSuperFollowsTweetFields: false,284withSuperFollowsUserFields: false,285})286);287if (Object.keys(features).length > 0) {288url.searchParams.set("features", JSON.stringify(features));289}290if (Object.keys(fieldToggles).length > 0) {291url.searchParams.set("fieldToggles", JSON.stringify(fieldToggles));292}293294const response = await fetch(url.toString(), {295headers: buildRequestHeaders(cookieMap, userAgent, bearerToken),296});297298const text = await response.text();299if (!response.ok) {300throw new Error(`X API error (${response.status}): ${text.slice(0, 400)}`);301}302303try {304return JSON.parse(text);305} catch (error) {306throw new Error(`Failed to parse response JSON: ${error instanceof Error ? error.message : String(error)}`);307}308}309310async function fetchArticleEntityById(311articleEntityId: string,312cookieMap: Record<string, string>,313userAgent: string,314bearerToken: string315): Promise<unknown> {316const queryInfo = await resolveArticleQueryInfo(userAgent);317const features = buildFeatureMap(queryInfo.html, queryInfo.featureSwitches);318const fieldToggles = buildFieldToggleMap(queryInfo.fieldToggles);319320const url = new URL(`https://x.com/i/api/graphql/${queryInfo.queryId}/ArticleEntityResultByRestId`);321url.searchParams.set("variables", JSON.stringify({ articleEntityId }));322if (Object.keys(features).length > 0) {323url.searchParams.set("features", JSON.stringify(features));324}325if (Object.keys(fieldToggles).length > 0) {326url.searchParams.set("fieldToggles", JSON.stringify(fieldToggles));327}328329const response = await fetch(url.toString(), {330headers: buildRequestHeaders(cookieMap, userAgent, bearerToken),331});332333const text = await response.text();334if (!response.ok) {335throw new Error(`X API error (${response.status}): ${text.slice(0, 400)}`);336}337338try {339return JSON.parse(text);340} catch (error) {341throw new Error(`Failed to parse response JSON: ${error instanceof Error ? error.message : String(error)}`);342}343}344345export async function fetchXArticle(346articleId: string,347cookieMap: Record<string, string>,348raw: boolean349): Promise<unknown> {350const userAgent = process.env.X_USER_AGENT?.trim() || DEFAULT_USER_AGENT;351const bearerToken = process.env.X_BEARER_TOKEN?.trim() || DEFAULT_BEARER_TOKEN;352353const tweetPayload = await fetchTweetResult(articleId, cookieMap, userAgent, bearerToken);354if (raw) {355return tweetPayload;356}357358const articleFromTweet = extractArticleFromTweet(tweetPayload);359if (isNonEmptyObject(articleFromTweet)) {360return articleFromTweet;361}362363const articlePayload = await fetchArticleEntityById(articleId, cookieMap, userAgent, bearerToken);364const articleFromEntity = extractArticleFromEntity(articlePayload);365if (isNonEmptyObject(articleFromEntity)) {366return articleFromEntity;367}368return articleFromEntity ?? articlePayload;369}370371export async function fetchXTweet(372tweetId: string,373cookieMap: Record<string, string>,374raw: boolean375): Promise<unknown> {376const userAgent = process.env.X_USER_AGENT?.trim() || DEFAULT_USER_AGENT;377const bearerToken = process.env.X_BEARER_TOKEN?.trim() || DEFAULT_BEARER_TOKEN;378379const tweetPayload = await fetchTweetResult(tweetId, cookieMap, userAgent, bearerToken);380if (raw) {381return tweetPayload;382}383384const tweet = extractTweetFromPayload(tweetPayload);385if (isNonEmptyObject(tweet)) {386return tweet;387}388return tweet ?? tweetPayload;389}390