Loads gated scripts for authorized faction members (ONLY available for CR)
目前為
// ==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();
})();