Greasy Fork is available in English.
Ferries x.com GraphQL responses to a local service. Patches the page's real window.fetch + XMLHttpRequest via Tampermonkey unsafeWindow (CSP-safe).
// ==UserScript==
// @name X Bridge (claw)
// @namespace https://github.com/Bug-Finderr/x-bridge
// @version 0.3.0
// @description Ferries x.com GraphQL responses to a local service. Patches the page's real window.fetch + XMLHttpRequest via Tampermonkey unsafeWindow (CSP-safe).
// @match https://x.com/*
// @run-at document-start
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect localhost
// @connect 127.0.0.1
// @license Apache-2.0
// ==/UserScript==
(function () {
'use strict';
const LOCAL = 'http://127.0.0.1:19816';
const CAPTURE_OPS = ['SearchTimeline', 'UserTweets', 'UserTweetsAndReplies', 'HomeTimeline', 'HomeLatestTimeline', 'TweetDetail'];
const POLL_MS = 5000;
const WATCHDOG_MS = 75000;
const params = new URLSearchParams(location.search);
const bridgeOn = params.has('bridge');
if (bridgeOn) sessionStorage.setItem('claw_bridge', '1');
const IS_BRIDGE = sessionStorage.getItem('claw_bridge') === '1';
const JOBID = params.get('jobid') || null;
const opFrom = (u) => {
const m = String(u).match(/\/i\/api\/graphql\/[^/]+\/([A-Za-z0-9_]+)/);
return m ? m[1] : null;
};
const gmPost = (path, body) =>
new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'POST',
url: LOCAL + path,
data: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
timeout: 8000,
onload: (r) => resolve(r),
onerror: () => resolve(null),
ontimeout: () => resolve(null),
});
});
const gmGet = (path) =>
new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: LOCAL + path,
timeout: 5000,
onload: (r) => { try { resolve(JSON.parse(r.responseText)); } catch { resolve(null); } },
onerror: () => resolve(null),
ontimeout: () => resolve(null),
});
});
const emit = (op, url, body) => {
console.log('[x-bridge] capture', op, (body || '').length, 'bytes');
gmPost('/captured', {
op, url, body,
jobid: JOBID,
captured_at: new Date().toISOString(),
});
};
// Patch the PAGE's real fetch/XHR via unsafeWindow. Tampermonkey extension
// privilege bypasses X's CSP (inline <script> injection is nonce-blocked).
try {
const W = unsafeWindow;
const origFetch = W.fetch.bind(W);
W.fetch = function (input, init) {
const url = typeof input === 'string' ? input : (input && input.url) || '';
const op = opFrom(url);
const p = origFetch(input, init);
if (op && CAPTURE_OPS.includes(op)) {
p.then((resp) => {
try { resp.clone().text().then((t) => emit(op, url, t)).catch(() => {}); } catch (_) {}
}).catch(() => {});
}
return p;
};
const OrigXHR = W.XMLHttpRequest;
function PatchedXHR() {
const xhr = new OrigXHR();
let _url = '';
const origOpen = xhr.open;
xhr.open = function (m, u) { _url = u; return origOpen.apply(xhr, arguments); };
xhr.addEventListener('load', function () {
const op = opFrom(_url);
if (op && CAPTURE_OPS.includes(op)) {
try { emit(op, _url, xhr.responseText); } catch (_) {}
}
});
return xhr;
}
PatchedXHR.prototype = OrigXHR.prototype;
W.XMLHttpRequest = PatchedXHR;
console.log('[x-bridge] interceptors installed on unsafeWindow');
} catch (e) {
console.error('[x-bridge] failed to patch unsafeWindow:', e);
}
if (!IS_BRIDGE) return;
console.log('[x-bridge] bridge mode, jobid=', JOBID);
if (JOBID) {
let finished = false;
const watchdog = setTimeout(() => {
if (finished) return;
console.log('[x-bridge] watchdog fired, aborting + home');
gmPost('/abort', { jobid: JOBID }).finally(() => { location.href = '/home?bridge=1'; });
}, WATCHDOG_MS);
const poll = setInterval(async () => {
const j = await gmGet('/queries');
if (!j) return;
const stillPending = Array.isArray(j.queue) && j.queue.some((q) => q.id === JOBID);
if (!stillPending) {
finished = true;
clearInterval(poll);
clearTimeout(watchdog);
console.log('[x-bridge] job done, home');
setTimeout(() => { location.href = '/home?bridge=1'; }, 300);
}
}, 2000);
} else {
setTimeout(function tick() {
gmGet('/queries').then((j) => {
if (j && Array.isArray(j.queue) && j.queue.length) {
const job = j.queue[0];
let target;
if (job.kind === 'search') {
const f = job.type === 'Latest' ? 'live' : 'top';
target = `/search?q=${encodeURIComponent(job.q)}&src=typed_query&f=${f}&bridge=1&jobid=${encodeURIComponent(job.id)}`;
} else if (job.kind === 'tweet') {
target = `/i/status/${encodeURIComponent(job.tweet_id)}?bridge=1&jobid=${encodeURIComponent(job.id)}`;
} else {
setTimeout(tick, POLL_MS); return;
}
console.log('[x-bridge] picking job', job.id, target);
location.href = target;
return;
}
setTimeout(tick, POLL_MS);
});
}, 1500);
}
})();