Loads gated scripts for authorized faction members
Pada tanggal
// ==UserScript==
// @name Torn Script Loader
// @namespace https://github.com/torn-script-loader
// @version 3.1.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 VERSION = GM_info.script.version;
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] === true;
}
// ── 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 ─────────────────────────────────────────────────────
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 ─────────────────────────────────────────────
let panelOpen = false;
let currentManifest = [];
// Gear SVG matching Torn's settings-menu icon style (28x28 viewBox)
const GEAR_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="-6 -4 28 28"><path
d="M15.93,7.16l-1.41-.37a.46.46,0,0,1-.32-.32,6.16,6.16,0,0,0-.34-.83.45.45,0,0,1,0-.45l.74-1.25a.45.45,0,0,0-.07
-.55l-.93-.93a.45.45,0,0,0-.55-.07l-1.25.74a.45.45,0,0,1-.45,0,6.16,6.16,0,0,0-.83-.34.46.46,0,0,1-.32-.32L9.84.0
7A.45.45,0,0,0,9.4-.27H7.6a.45.45,0,0,0-.44.34L6.79,1.48a.46.46,0,0,1-.32.32,6.16,6.16,0,0,0-.83.34.45.45,0,0,1-.
45,0L3.94,1.39a.45.45,0,0,0-.55.07l-.93.93a.45.45,0,0,0-.07.55l.74,1.25a.45.45,0,0,1,0,.45,6.16,6.16,0,0,0-.34.83
.46.46,0,0,1-.32.32L1.07,6.16A.45.45,0,0,0,.73,6.6V8.4a.45.45,0,0,0,.34.44l1.41.37a.46.46,0,0,1,.32.32,6.16,6.16,
0,0,0,.34.83.45.45,0,0,1,0,.45L2.39,11.06a.45.45,0,0,0,.07.55l.93.93a.45.45,0,0,0,.55.07l1.25-.74a.45.45,0,0,1,.4
5,0,6.16,6.16,0,0,0,.83.34.46.46,0,0,1,.32.32l.37,1.41a.45.45,0,0,0,.44.34h1.8a.45.45,0,0,0,.44-.34l.37-1.41a.46.
46,0,0,1,.32-.32,6.16,6.16,0,0,0,.83-.34.45.45,0,0,1,.45,0l1.25.74a.45.45,0,0,0,.55-.07l.93-.93a.45.45,0,0,0,.07-
.55l-.74-1.25a.45.45,0,0,1,0-.45,6.16,6.16,0,0,0,.34-.83.46.46,0,0,1,.32-.32l1.41-.37A.45.45,0,0,0,16.27,8.4V6.6A
.45.45,0,0,0,15.93,7.16ZM8.5,10.75A3.25,3.25,0,1,1,11.75,7.5,3.25,3.25,0,0,1,8.5,10.75Z"></path></svg>`;
function createPanel() {
if (document.getElementById('sl-panel')) return;
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);
// Click outside closes
document.addEventListener('mousedown', (e) => {
if (!panelOpen) return;
if (panel.contains(e.target)) return;
if (e.target.closest && e.target.closest('#sl-menu-item')) return;
panelOpen = false;
panel.style.display = 'none';
});
}
function togglePanel() {
const panel = document.getElementById('sl-panel');
if (!panel) return;
panelOpen = !panelOpen;
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');
while (panel.firstChild) panel.removeChild(panel.firstChild);
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 = `v${VERSION}`;
Object.assign(version.style, { fontSize: '11px', color: '#666' });
header.append(title, version);
panel.appendChild(header);
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);
}
}
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);
}
// ── Inject into Torn settings dropdown ─────────────────────────────
function injectMenuItem() {
const tryInject = () => {
const menu = document.querySelector('ul.settings-menu');
if (!menu) return false;
if (menu.querySelector('#sl-menu-item')) return true;
const item = document.createElement('li');
item.id = 'sl-menu-item';
item.className = 'link';
const a = document.createElement('a');
a.href = '#';
a.style.cursor = 'pointer';
const iconWrap = document.createElement('div');
iconWrap.className = 'icon-wrapper';
iconWrap.innerHTML = GEAR_SVG;
const text = document.createElement('span');
text.className = 'link-text';
text.textContent = 'Scripts';
a.append(iconWrap, text);
item.appendChild(a);
a.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
togglePanel();
});
// Insert after View Profile (first child)
if (menu.firstChild && menu.firstChild.nextSibling) {
menu.insertBefore(item, menu.firstChild.nextSibling);
} else {
menu.appendChild(item);
}
return true;
};
if (tryInject()) return;
const observer = new MutationObserver(() => { tryInject(); });
observer.observe(document.body, { childList: true, subtree: true });
}
// ── Main ───────────────────────────────────────────────────────────
async function main() {
let apiKey = getApiKey();
if (!apiKey) {
apiKey = promptForApiKey();
if (!apiKey) return;
}
setupXhrBridge();
createPanel();
injectMenuItem();
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();
})();