Torn Script Loader

Loads gated scripts for authorized faction members (ONLY available for CR)

2026-02-26 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Torn Script Loader
// @namespace    https://github.com/torn-script-loader
// @version      1.0.0
// @author       Legaci [2100546]
// @description  Loads gated scripts for authorized faction members (ONLY available for CR)
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addElement
// @connect      torn-script-loader.theaaronlawrence.workers.dev
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ── Configuration ──────────────────────────────────────────────────
  const WORKER_BASE = 'https://torn-script-loader.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);
        },
      });
    });
  }

  const GM_POLYFILLS = `
    if (typeof GM_addStyle === 'undefined') {
      window.GM_addStyle = function(css) {
        var s = document.createElement('style');
        s.textContent = css;
        document.head.appendChild(s);
        return s;
      };
    }
    if (typeof GM_getValue === 'undefined') {
      window.GM_getValue = function(key, def) {
        var v = localStorage.getItem('_gm_' + key);
        return v === null ? def : JSON.parse(v);
      };
    }
    if (typeof GM_setValue === 'undefined') {
      window.GM_setValue = function(key, val) {
        localStorage.setItem('_gm_' + key, JSON.stringify(val));
      };
    }
  `;

  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;
    }

    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();
})();