Greasy Fork is available in English.

Torn Script Loader

Loads gated scripts for authorized faction members

Per 27-02-2026. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Torn Script Loader
// @namespace    https://github.com/torn-script-loader
// @version      3.0.0
// @description  Loads gated scripts for authorized faction members
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addElement
// @connect      torn-script-loader-v2.theaaronlawrence.workers.dev
// @connect      *
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // ── Configuration ──────────────────────────────────────────────────
  const WORKER_BASE = 'https://torn-script-loader-v2.theaaronlawrence.workers.dev';
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

  // ── API Key Management ─────────────────────────────────────────────
  function getApiKey() {
    return GM_getValue('torn_api_key', null);
  }

  function setApiKey(key) {
    GM_setValue('torn_api_key', key);
  }

  function clearApiKey() {
    GM_setValue('torn_api_key', null);
    GM_setValue('cache_manifest', null);
  }

  function promptForApiKey() {
    const key = prompt(
      '[Script Loader] Enter your Torn API key.\n' +
      'This is used to verify your faction membership.\n' +
      'You can generate one at Settings > API Key.'
    );
    if (key && key.trim().length === 16) {
      setApiKey(key.trim());
      return key.trim();
    }
    if (key !== null) {
      alert('[Script Loader] Invalid API key. Must be 16 characters. Reload to try again.');
    }
    return null;
  }

  // ── Enable/Disable State ───────────────────────────────────────────
  function getEnabledScripts() {
    return GM_getValue('enabled_scripts', {});
  }

  function setScriptEnabled(name, enabled) {
    const state = getEnabledScripts();
    state[name] = enabled;
    GM_setValue('enabled_scripts', state);
  }

  function isScriptEnabled(name) {
    const state = getEnabledScripts();
    return state[name] !== false;
  }

  // ── Cache ──────────────────────────────────────────────────────────
  function getCached(key) {
    const raw = GM_getValue(`cache_${key}`, null);
    if (!raw) return null;
    try {
      const cached = JSON.parse(raw);
      if (Date.now() - cached.ts < CACHE_TTL) {
        return cached.data;
      }
    } catch { /* cache miss */ }
    return null;
  }

  function setCache(key, data) {
    GM_setValue(`cache_${key}`, JSON.stringify({ ts: Date.now(), data }));
  }

  // ── Manifest Fetch ─────────────────────────────────────────────────
  function fetchManifest(apiKey) {
    return new Promise((resolve, reject) => {
      const cached = getCached('manifest');
      if (cached) {
        resolve(cached);
        return;
      }

      GM_xmlhttpRequest({
        method: 'POST',
        url: `${WORKER_BASE}/manifest`,
        headers: { 'Content-Type': 'application/json' },
        data: JSON.stringify({ apiKey }),
        onload(response) {
          try {
            const data = JSON.parse(response.responseText);
            if (data.ok && data.manifest) {
              setCache('manifest', data.manifest);
              resolve(data.manifest);
            } else {
              showError(data.error || 'Failed to load manifest');
              reject(new Error(data.error));
            }
          } catch (e) {
            showError('Failed to parse manifest response');
            reject(e);
          }
        },
        onerror(err) {
          showError('Network error fetching manifest');
          reject(err);
        },
      });
    });
  }

  // ── Script Loading ─────────────────────────────────────────────────
  function loadScript(scriptName, apiKey) {
    return new Promise((resolve, reject) => {
      const cached = getCached(scriptName);
      if (cached) {
        injectScript(cached, scriptName);
        resolve(true);
        return;
      }

      GM_xmlhttpRequest({
        method: 'POST',
        url: `${WORKER_BASE}/load`,
        headers: { 'Content-Type': 'application/json' },
        data: JSON.stringify({ apiKey, script: scriptName }),
        onload(response) {
          try {
            const data = JSON.parse(response.responseText);
            if (data.ok && data.code) {
              setCache(scriptName, data.code);
              injectScript(data.code, scriptName);
              resolve(true);
            } else {
              showError(data.error || 'Unknown error loading script');
              reject(new Error(data.error));
            }
          } catch (e) {
            showError('Failed to parse worker response');
            reject(e);
          }
        },
        onerror(err) {
          showError('Network error contacting script loader');
          reject(err);
        },
      });
    });
  }

  // Polyfills injected into the page context (via GM_addElement).
  // GM_xmlhttpRequest uses postMessage to bridge back to the loader's
  // Tampermonkey context, which has the real GM_xmlhttpRequest that
  // bypasses CSP restrictions.
  const GM_POLYFILLS = `
    window.GM_addStyle = function(css) {
      var s = document.createElement('style');
      s.textContent = css;
      document.head.appendChild(s);
      return s;
    };
    window.GM_getValue = function(key, def) {
      var v = localStorage.getItem('_gm_' + key);
      return v === null ? def : JSON.parse(v);
    };
    window.GM_setValue = function(key, val) {
      localStorage.setItem('_gm_' + key, JSON.stringify(val));
    };
    window.GM_xmlhttpRequest = function(opts) {
      var id = 'sl_xhr_' + Math.random().toString(36).slice(2);
      function handler(event) {
        if (!event.data || event.data.type !== 'SL_XHR_RESPONSE' || event.data.id !== id) return;
        window.removeEventListener('message', handler);
        if (event.data.error) {
          if (opts.onerror) opts.onerror(event.data.error);
        } else {
          if (opts.onload) opts.onload(event.data.response);
        }
      }
      window.addEventListener('message', handler);
      window.postMessage({
        type: 'SL_XHR_REQUEST',
        id: id,
        opts: {
          method: opts.method,
          url: opts.url,
          headers: opts.headers,
          data: opts.data,
          responseType: opts.responseType
        }
      }, '*');
    };
  `;

  // ── XHR Bridge ─────────────────────────────────────────────────────
  // Listens for postMessage requests from injected scripts and proxies
  // them through the real GM_xmlhttpRequest (which bypasses page CSP).
  function setupXhrBridge() {
    window.addEventListener('message', (event) => {
      if (!event.data || event.data.type !== 'SL_XHR_REQUEST') return;

      const { id, opts } = event.data;
      GM_xmlhttpRequest({
        method: opts.method || 'GET',
        url: opts.url,
        headers: opts.headers || {},
        data: opts.data || undefined,
        responseType: opts.responseType || '',
        onload(response) {
          window.postMessage({
            type: 'SL_XHR_RESPONSE',
            id,
            response: {
              status: response.status,
              statusText: response.statusText,
              responseText: response.responseText,
              response: response.response,
              finalUrl: response.finalUrl,
            },
          }, '*');
        },
        onerror(err) {
          window.postMessage({
            type: 'SL_XHR_RESPONSE',
            id,
            error: true,
          }, '*');
        },
      });
    });
  }

  function injectScript(code, name) {
    try {
      GM_addElement('script', { textContent: GM_POLYFILLS + '\n' + code });
      console.log(`[Script Loader] Loaded: ${name}`);
    } catch (e) {
      console.error(`[Script Loader] Error executing ${name}:`, e);
      showError(`Error running ${name}`);
    }
  }

  // ── UI: Error Toast ────────────────────────────────────────────────
  function showError(message) {
    console.warn(`[Script Loader] ${message}`);
    const el = document.createElement('div');
    el.textContent = `Script Loader: ${message}`;
    Object.assign(el.style, {
      position: 'fixed',
      bottom: '10px',
      right: '10px',
      background: '#c0392b',
      color: '#fff',
      padding: '8px 14px',
      borderRadius: '6px',
      fontSize: '13px',
      zIndex: '99999',
      opacity: '0.95',
      fontFamily: 'Arial, sans-serif',
    });
    document.body.appendChild(el);
    setTimeout(() => el.remove(), 6000);
  }

  // ── UI: Settings Panel (DOM-based, no innerHTML) ───────────────────
  let panelOpen = false;
  let currentManifest = [];

  function createSettingsPanel() {
    // Gear button
    const gear = document.createElement('div');
    gear.id = 'sl-gear';
    gear.textContent = '\u2699';
    Object.assign(gear.style, {
      position: 'fixed',
      top: '14px',
      right: '14px',
      width: '34px',
      height: '34px',
      background: '#333',
      color: '#aaa',
      borderRadius: '50%',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      fontSize: '18px',
      cursor: 'pointer',
      zIndex: '100000',
      border: '1px solid #444',
      userSelect: 'none',
      transition: 'background 0.15s, color 0.15s',
    });
    gear.addEventListener('mouseenter', () => { gear.style.background = '#444'; gear.style.color = '#ddd'; });
    gear.addEventListener('mouseleave', () => { gear.style.background = '#333'; gear.style.color = '#aaa'; });
    gear.addEventListener('click', togglePanel);
    document.body.appendChild(gear);

    // Panel container
    const panel = document.createElement('div');
    panel.id = 'sl-panel';
    Object.assign(panel.style, {
      position: 'fixed',
      top: '56px',
      right: '14px',
      width: '280px',
      maxHeight: '400px',
      overflowY: 'auto',
      background: '#1a1a2e',
      border: '1px solid #333',
      borderRadius: '8px',
      zIndex: '100000',
      fontFamily: 'Arial, sans-serif',
      fontSize: '13px',
      color: '#ccc',
      display: 'none',
      boxShadow: '0 4px 20px rgba(0,0,0,0.5)',
    });
    document.body.appendChild(panel);
  }

  function togglePanel() {
    panelOpen = !panelOpen;
    const panel = document.getElementById('sl-panel');
    panel.style.display = panelOpen ? 'block' : 'none';
    if (panelOpen) renderPanel();
  }

  function makeToggle(name) {
    const enabled = isScriptEnabled(name);

    const label = document.createElement('label');
    Object.assign(label.style, {
      position: 'relative',
      display: 'inline-block',
      width: '40px',
      height: '22px',
      marginLeft: '10px',
      flexShrink: '0',
      cursor: 'pointer',
    });

    const input = document.createElement('input');
    input.type = 'checkbox';
    input.checked = enabled;
    Object.assign(input.style, { opacity: '0', width: '0', height: '0', position: 'absolute' });

    const track = document.createElement('span');
    Object.assign(track.style, {
      position: 'absolute', top: '0', left: '0', right: '0', bottom: '0',
      background: enabled ? '#4CAF50' : '#555',
      borderRadius: '11px', transition: 'background 0.2s',
    });

    const thumb = document.createElement('span');
    Object.assign(thumb.style, {
      position: 'absolute', top: '2px', left: enabled ? '20px' : '2px',
      width: '18px', height: '18px', background: '#fff', borderRadius: '50%',
      transition: 'left 0.2s',
    });

    input.addEventListener('change', () => {
      const on = input.checked;
      setScriptEnabled(name, on);
      track.style.background = on ? '#4CAF50' : '#555';
      thumb.style.left = on ? '20px' : '2px';
    });

    label.append(input, track, thumb);
    return label;
  }

  function renderPanel() {
    const panel = document.getElementById('sl-panel');
    // Clear previous content
    while (panel.firstChild) panel.removeChild(panel.firstChild);

    // ── Header ──
    const header = document.createElement('div');
    Object.assign(header.style, {
      padding: '12px 14px 8px',
      borderBottom: '1px solid #333',
      display: 'flex',
      justifyContent: 'space-between',
      alignItems: 'center',
    });
    const title = document.createElement('span');
    title.textContent = 'Script Loader';
    Object.assign(title.style, { fontWeight: 'bold', fontSize: '14px', color: '#eee' });
    const version = document.createElement('span');
    version.textContent = 'v2.0';
    Object.assign(version.style, { fontSize: '11px', color: '#666' });
    header.append(title, version);
    panel.appendChild(header);

    // ── Script rows ──
    if (currentManifest.length === 0) {
      const empty = document.createElement('div');
      empty.textContent = 'No scripts in manifest';
      Object.assign(empty.style, { padding: '14px', color: '#666', textAlign: 'center' });
      panel.appendChild(empty);
    } else {
      for (const entry of currentManifest) {
        const row = document.createElement('div');
        Object.assign(row.style, {
          padding: '10px 14px',
          borderBottom: '1px solid #262640',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
        });

        const info = document.createElement('div');
        Object.assign(info.style, { flex: '1', minWidth: '0' });

        const nameEl = document.createElement('div');
        nameEl.textContent = entry.name;
        Object.assign(nameEl.style, { fontWeight: 'bold', color: '#ddd', marginBottom: '2px' });
        info.appendChild(nameEl);

        if (entry.description) {
          const desc = document.createElement('div');
          desc.textContent = entry.description;
          Object.assign(desc.style, {
            fontSize: '11px', color: '#777',
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          });
          info.appendChild(desc);
        }

        row.append(info, makeToggle(entry.name));
        panel.appendChild(row);
      }
    }

    // ── Footer: Reset API key ──
    const footer = document.createElement('div');
    Object.assign(footer.style, { padding: '10px 14px', borderTop: '1px solid #333' });

    const resetBtn = document.createElement('button');
    resetBtn.textContent = 'Reset API Key';
    Object.assign(resetBtn.style, {
      background: 'none', border: '1px solid #555', color: '#999',
      padding: '5px 10px', borderRadius: '4px', fontSize: '11px',
      cursor: 'pointer', width: '100%', transition: 'background 0.15s, color 0.15s',
    });
    resetBtn.addEventListener('mouseenter', () => {
      resetBtn.style.background = '#c0392b'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#c0392b';
    });
    resetBtn.addEventListener('mouseleave', () => {
      resetBtn.style.background = 'none'; resetBtn.style.color = '#999'; resetBtn.style.borderColor = '#555';
    });
    resetBtn.addEventListener('click', () => {
      if (confirm('Reset your API key? You will need to re-enter it on next page load.')) {
        clearApiKey();
        panelOpen = false;
        document.getElementById('sl-panel').style.display = 'none';
        showError('API key cleared. Reload the page to re-enter.');
      }
    });

    footer.appendChild(resetBtn);
    panel.appendChild(footer);
  }

  // ── Main ───────────────────────────────────────────────────────────
  async function main() {
    let apiKey = getApiKey();
    if (!apiKey) {
      apiKey = promptForApiKey();
      if (!apiKey) return;
    }

    setupXhrBridge();
    createSettingsPanel();

    let manifest;
    try {
      manifest = await fetchManifest(apiKey);
    } catch {
      return;
    }

    currentManifest = manifest;
    const currentUrl = window.location.href;

    for (const entry of manifest) {
      if (!entry.name || !entry.match) continue;

      const pattern = new RegExp(entry.match);
      if (!pattern.test(currentUrl)) continue;
      if (!isScriptEnabled(entry.name)) continue;

      loadScript(entry.name, apiKey);
    }
  }

  main();
})();