<3/products/browser/Sources/GhostBrowser/Resources/

Capture.js

(() => {
  'use strict';
  if (window !== window.top || window.__ghostInstalled) return;
  window.__ghostInstalled = true;
  const adapter = window.GhostXAdapter;
  const bridge = window.webkit?.messageHandlers?.ghost;
  if (!bridge) return;
  const send = value => { try { bridge.postMessage(value); } catch {} };
  let current = location.href;
  let intent = null;
  let paused = false;
  let generation = 0;
  window.__ghostSetPaused = value => { paused = !!value; generation++; intent = null; };
  const http = value => { try { return /^https?:$/.test(new URL(value, location.href).protocol); } catch { return false; } };
  const navigation = () => {
    const next = location.href;
    if (next === current) return;
    const source = current; current = next;
    // Record deliberate SPA arrivals, not every replaceState/URL decoration.
    if (!paused && intent && Date.now() - intent.time < 10000 && http(next)) {
      send({ kind: 'navigation', url: next, source_url: intent.source || source, navigation_kind: intent.kind, ...adapter.navigation(next, intent.source || source) });
    }
    intent = null;
  };
  document.addEventListener('click', event => {
    if (!event.isTrusted || paused) return;
    const link = event.target?.closest?.('a[href]');
    if (link && http(link.href)) {
      const context = { kind: 'link', url: link.href, source_url: location.href, ...adapter.navigation(link.href, location.href) };
      send(context);
      if (!event.metaKey && !event.ctrlKey && !event.shiftKey && link.target !== '_blank') intent = { source: location.href, time: Date.now(), kind: 'link' };
    } else if (!event.target?.closest?.('input,textarea,[contenteditable="true"]')) {
      // Sites also navigate from clickable cards and divs. Keep only transient source
      // context; a click itself is never recorded, nor are labels or form contents.
      intent = { source: location.href, time: Date.now(), kind: 'site_navigation' };
    }
  }, true);
  for (const name of ['pushState', 'replaceState']) {
    const original = history[name];
    history[name] = function (...args) { const result = original.apply(this, args); navigation(); return result; };
  }
  window.addEventListener('popstate', () => { intent = { source: current, time: Date.now(), kind: 'back_forward' }; navigation(); });
  window.addEventListener('hashchange', navigation);
  send({ kind: 'ready', adapter: adapter.xURL(location.href) ? 'x' : 'navigation' });
  if (!adapter.xURL(location.href)) return;

  function postSnapshot(body) {
    const id = adapter.variables(body).tweet_id;
    if (!id) return null;
    const articles = document.querySelectorAll('article[data-testid="tweet"]');
    for (const article of articles) {
      const permalink = Array.from(article.querySelectorAll('a[href*="/status/"]')).find(a => a.querySelector('time') && new URL(a.href).pathname.endsWith(`/status/${id}`));
      if (!permalink) continue;
      const handle = new URL(permalink.href).pathname.split('/')[1];
      const media = Array.from(article.querySelectorAll('[data-testid="tweetPhoto"] img')).slice(0, 16).map(img => ({ url: img.currentSrc || img.src, alt: img.alt || null }));
      return { url: permalink.href, author: { handle }, text: article.querySelector('[data-testid="tweetText"]')?.innerText || '', media };
    }
    return null;
  }
  function context(body) { return { source_url: location.href, post: postSnapshot(body) }; }
  function capture(request, response, status, ctx, epoch) {
    if (paused || epoch !== generation) return;
    const event = adapter.normalize({ ...request, response, status, context: ctx });
    if (event) send({ kind: 'event', ...event });
    else if (status >= 200 && status < 300) {
      let result; try { result = typeof response === 'string' ? JSON.parse(response) : response; } catch {}
      if (!result?.errors?.length && !result?.error) send({ kind: 'capture_problem', message: 'X returned an unfamiliar response. This action could not be recorded accurately.' });
    }
  }

  // Observe only the allowlisted writes; never retain headers, cookies or unrelated requests.
  const originalFetch = window.fetch;
  window.fetch = function (input, init) {
    const url = typeof input === 'string' || input instanceof URL ? new URL(String(input), location.href).href : input.url;
    const method = init?.method || input?.method || 'GET';
    if (paused || !adapter.operation(url, method)) return originalFetch.apply(this, arguments);
    const epoch = generation, source = location.href;
    const bodyPromise = typeof init?.body === 'string' ? Promise.resolve(init.body) : input instanceof Request ? input.clone().text() : Promise.resolve('');
    const ctxPromise = bodyPromise.then(body => ({ ...context(body), source_url: source }));
    const result = originalFetch.apply(this, arguments);
    result.then(async response => {
      try { const copy = response.clone(); const body = await bodyPromise; const ctx = await ctxPromise; capture({ url, method, body }, await copy.text(), response.status, ctx, epoch); } catch {
        if (!paused && epoch === generation) send({ kind: 'capture_problem', message: 'Could not inspect an X action response.' });
      }
    }, () => {});
    return result;
  };
  const xhr = XMLHttpRequest.prototype;
  const originalOpen = xhr.open, originalSend = xhr.send;
  const requests = new WeakMap();
  xhr.open = function (method, url, ...rest) {
    requests.delete(this);
    const absolute = new URL(String(url), location.href).href;
    if (adapter.operation(absolute, method)) requests.set(this, { method, url: absolute });
    return originalOpen.call(this, method, url, ...rest);
  };
  xhr.send = function (body) {
    const request = requests.get(this);
    if (request && !paused) {
      const epoch = generation, ctx = context(body);
      this.addEventListener('load', () => {
        try {
          const response = this.responseType === 'json' ? this.response : this.responseText;
          capture({ ...request, body }, response, this.status, ctx, epoch);
        } catch { if (!paused && epoch === generation) send({ kind: 'capture_problem', message: 'Could not inspect an X action response.' }); }
      }, { once: true });
    }
    return originalSend.apply(this, arguments);
  };
})();