← back to Axel Bitblaze Scout
scripts/hydrate-tweet.js
133 lines
#!/usr/bin/env node
/**
* hydrate-tweet.js — resolve a single X/Twitter post to plain text WITHOUT auth.
*
* X blocks unauthenticated page reads (WebFetch → HTTP 402), so this uses the
* public syndication endpoint that powers embedded tweets. The endpoint needs a
* `token` that is a deterministic function of the tweet id — we compute it
* locally ($0), so no API key and no login are ever required.
*
* Usage:
* node hydrate-tweet.js <tweet-id-or-url>
*
* Examples:
* node hydrate-tweet.js 2055557527153332556
* node hydrate-tweet.js https://x.com/heygurisingh/status/2055557527153332556
*
* Exit codes:
* 0 ok — printed tweet JSON summary
* 3 tombstone — tweet/account is suspended, deleted, or age-gated (unreadable)
* 1 error — bad input or network/parse failure
*
* Output: a JSON object on stdout:
* { id, url, author, created_at, text, urls[], github[] }
* On tombstone: { id, tombstone: "<reason text>" } and exit 3.
*/
'use strict';
function extractId(arg) {
if (!arg) return null;
// trailing run of digits handles both bare ids and .../status/<id>[?query]
const m = String(arg).match(/(\d{5,25})/g);
return m ? m[m.length - 1] : null;
}
// react-tweet's documented token derivation: (id / 1e15 * PI) in base-36,
// with zeros and the dot stripped. Verified against live tweets 2026-07.
function syndicationToken(id) {
return ((Number(id) / 1e15) * Math.PI)
.toString(36)
.replace(/(0+|\.)/g, '');
}
function findGithub(text, urls) {
const found = new Set();
const scan = (s) => {
if (!s) return;
const re = /https?:\/\/(?:www\.)?github\.com\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+/g;
let m;
while ((m = re.exec(s))) found.add(m[0].replace(/[.,)]+$/, ''));
};
scan(text);
(urls || []).forEach(scan);
return [...found];
}
async function main() {
const id = extractId(process.argv[2]);
if (!id) {
console.error('usage: node hydrate-tweet.js <tweet-id-or-url>');
process.exit(1);
}
const token = syndicationToken(id);
const endpoint =
`https://cdn.syndication.twimg.com/tweet-result?id=${id}&lang=en&token=${token}`;
let res;
try {
res = await fetch(endpoint, {
headers: {
// a browser-ish UA avoids the occasional bot 403 on this endpoint
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/125.0 Safari/537.36',
Accept: 'application/json',
},
});
} catch (e) {
console.error(`network error: ${e.message}`);
process.exit(1);
}
if (res.status === 404) {
console.log(JSON.stringify({ id, tombstone: 'not found (404)' }, null, 2));
process.exit(3);
}
if (!res.ok) {
console.error(`http ${res.status} from syndication endpoint`);
process.exit(1);
}
let data;
try {
data = await res.json();
} catch (e) {
console.error(`parse error: ${e.message}`);
process.exit(1);
}
// Tombstone shapes: suspended/deleted/age-gated tweets come back as a
// __typename of TweetTombstone (or an object with a `tombstone` field).
const tomb =
data.__typename === 'TweetTombstone' ||
data.tombstone ||
(data.text && /suspended account|Post is from a suspended|isn.?t available/i.test(data.text) && !data.user);
if (tomb) {
const reason =
(data.tombstone && (data.tombstone.text?.text || data.tombstone.text)) ||
data.text ||
'unavailable (suspended / deleted / age-gated)';
console.log(JSON.stringify({ id, tombstone: reason }, null, 2));
process.exit(3);
}
const urls = (data.entities?.urls || []).map((u) => u.expanded_url || u.url);
const out = {
id,
url: `https://x.com/${data.user?.screen_name || 'i'}/status/${id}`,
author: data.user
? `@${data.user.screen_name} (${data.user.name})`
: null,
created_at: data.created_at || null,
text: data.text || '',
urls,
github: findGithub(data.text || '', urls),
};
console.log(JSON.stringify(out, null, 2));
process.exit(0);
}
main();