Rostoll's Secure Keymaster (Profile Edition)

A modular, secure API key manager with native TornPDA support and dynamic user detection.

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://update.greatest.deepsurf.us/scripts/575462/1808747/Rostoll%27s%20Secure%20Keymaster%20%28Profile%20Edition%29.js

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Rostoll's Keymaster Library
// @namespace    https://greatest.deepsurf.us/en/users/1594626-rostoll-3936240
// @version      4.0
// @description  Secure API key manager library for Rostoll's Torn scripts.
// @author       Rostoll [3936240]
// @license      MIT
// @match        https://www.torn.com/*
// @grant        none
// ==/UserScript==

/* jshint esversion: 11 */

class RostollKeymaster {
    constructor(config) {
        this.scriptName = config.scriptName || 'Unknown Script';
        this.storageKey = config.storageKey || 'rst_default_api_key';
        this.pdaToken = config.pdaToken || '';
        this.isPDA = this.pdaToken.indexOf("#") !== 0 && this.pdaToken.length === 16;
        
        this.init();
    }

    async getKey() {
        if (this.isPDA) return this.pdaToken;
        const key = await GM_getValue(this.storageKey, '');
        return key.length === 16 ? key : null;
    }

    init() {
        if (window.self !== window.top) return; // Anti-iframe lock

        this.injectStyles();
        this.checkGlobalKey();
        this.injectProfileUI();
    }

    injectStyles() {
        if (document.getElementById('rst-keymaster-styles')) return;

        const style = document.createElement('style');
        style.id = 'rst-keymaster-styles';
        style.innerHTML = `
            #rst-profile-settings { margin: 10px 0; background: #242424; border-radius: 5px; border: 1px solid #333; color: #ccc; font-family: Arial, sans-serif; overflow: hidden; box-shadow: 0px 2px 4px rgba(0,0,0,0.5); }
            #rst-prof-header { background: #333; padding: 10px 15px; cursor: pointer; font-weight: bold; display: flex; align-items: center; color: #ddd; transition: background 0.2s; }
            #rst-prof-header:hover { background: #3d3d3d; }
            #rst-prof-icon { margin-right: 8px; font-size: 12px; transition: transform 0.2s; }
            #rst-prof-body { display: none; padding: 15px; border-top: 1px solid #444; }
            .rst-script-block { border-bottom: 1px dashed #444; padding-bottom: 15px; margin-bottom: 15px; }
            .rst-script-block:last-child { border-bottom: none; padding-bottom: 0; margin-bottom: 0; }
            .rst-prof-row { display: flex; align-items: center; margin-bottom: 10px; gap: 15px; }
            .rst-script-title { font-size: 14px; font-weight: bold; color: #4caf50; margin-bottom: 10px; }
            .rst-prof-label { min-width: 80px; font-size: 13px; color: #aaa; }
            .rst-api-input { flex: 1; max-width: 250px; background: #111; border: 1px solid #555; color: #4caf50; padding: 6px 10px; border-radius: 4px; font-family: monospace; font-size: 14px; letter-spacing: 1px; transition: filter 0.3s, border-color 0.3s; }
            .rst-api-input:focus { outline: none; border-color: #4caf50; filter: blur(0px) !important; }
            .rst-blur { filter: blur(4px); }
            .rst-btn { padding: 6px 12px; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; font-size: 11px; transition: background 0.2s; }
            .rst-btn-verify { background: #333; color: #fff; border: 1px solid #555; }
            .rst-btn-verify:hover { background: #444; }
            .rst-btn-clear { background: #5a1e1e; color: #fff; border: 1px solid #7a2828; }
            .rst-btn-clear:hover { background: #7a2828; }
            .rst-status-box { font-size: 11px; font-weight: bold; margin-top: 5px; }
            .rst-status-bad { color: #ff6600; }
            .rst-status-good { color: #4caf50; }
            .rst-status-wait { color: #88beff; }
            #rst-global-banner { position: relative; display: block; width: 100%; background: #2a0808; border-bottom: 2px solid #ff4444; color: #fff; text-align: center; padding: 10px 5px; font-family: sans-serif; font-size: 13px; z-index: 9999999; box-sizing: border-box; }
            #rst-global-banner a { color: #ffaa00; font-weight: bold; text-decoration: none; margin-left: 5px; }
            #rst-global-banner a:hover { text-decoration: underline; }
        `;
        document.head.appendChild(style);
    }

    checkGlobalKey() {
        if (this.isPDA) return;
        const key = GM_getValue(this.storageKey, '');
        const isProfilePage = window.location.href.includes('profiles.php');

        if (key.length !== 16 && !isProfilePage) {
            if (!document.getElementById('rst-global-banner')) {
                const banner = document.createElement('div');
                banner.id = 'rst-global-banner';
                banner.innerHTML = `⚠️ A Rostoll Script requires an API Key. <a href="/profiles.php?rstFocus=true">Tap here to set it up ➔</a>`;
                document.body.prepend(banner);
            }
        }
    }

    injectProfileUI() {
        if (!window.location.href.includes('profiles.php')) return;

        const myIdMatch = document.cookie.match(/uid=(\d+)/);
        const myId = myIdMatch ? myIdMatch[1] : null;

        const urlParams = new URLSearchParams(window.location.search);
        const pageXid = urlParams.get('XID');
        const shouldFocus = urlParams.get('rstFocus') === 'true';

        if (pageXid && pageXid !== myId) return;

        let attempts = 0;
        const waitInterval = setInterval(() => {
            attempts++;
            if (attempts >= 20) return clearInterval(waitInterval);

            const allHeaders = Array.from(document.querySelectorAll('div[class*="title"]'));
            const medalsHeader = allHeaders.find(h => h.textContent.trim() === 'Medals' || h.textContent.includes('Medals'));
            
            let targetAnchor = medalsHeader ? medalsHeader.closest('div[class*="box-info"], div[class*="profile-wrapper"], div[class*="mt-"]') : null;
            if (!targetAnchor) {
                const basicInfoHeader = allHeaders.find(h => h.textContent.includes('Basic Information'));
                targetAnchor = basicInfoHeader ? basicInfoHeader.closest('div[class*="box-info"], div[class*="profile-wrapper"], div[class*="mt-"]') : null;
            }

            if (!targetAnchor) return;
            clearInterval(waitInterval);

            this.buildAccordion(targetAnchor, shouldFocus);
        }, 500);
    }

    buildAccordion(targetAnchor, shouldFocus) {
        let accordion = document.getElementById('rst-profile-settings');
        let body;

        // Create the master accordion if it doesn't exist yet
        if (!accordion) {
            accordion = document.createElement('div');
            accordion.id = 'rst-profile-settings';
            accordion.innerHTML = `
                <div id="rst-prof-header">
                    <span id="rst-prof-icon">▶</span>
                    <span>Rostoll's Script Settings</span>
                </div>
                <div id="rst-prof-body">
                    <p style="font-size: 11px; margin-bottom: 15px; color: #888; border-bottom: 1px solid #444; padding-bottom: 10px;">
                        Manage your API Keys below. Each script requires its own key for maximum security. Keys are stored locally and never leave your browser.
                    </p>
                </div>
            `;
            targetAnchor.parentNode.insertBefore(accordion, targetAnchor);

            const header = document.getElementById('rst-prof-header');
            body = document.getElementById('rst-prof-body');
            const icon = document.getElementById('rst-prof-icon');

            header.addEventListener('click', () => {
                const isOpen = body.style.display === 'block';
                body.style.display = isOpen ? 'none' : 'block';
                icon.innerHTML = isOpen ? '▶' : '▼';
            });
        } else {
            body = document.getElementById('rst-prof-body');
        }

        // Check if this script's row already exists
        if (document.getElementById(`rst-block-${this.storageKey}`)) return;

        const savedKey = GM_getValue(this.storageKey, '');
        const hasKey = savedKey.length === 16;

        const block = document.createElement('div');
        block.className = 'rst-script-block';
        block.id = `rst-block-${this.storageKey}`;

        let uiContent = this.isPDA ? `
            <div class="rst-prof-row">
                <span class="rst-prof-label">API Key:</span>
                <span style="color: #888; font-family: monospace; font-size: 12px;">Managed securely by Torn PDA</span>
            </div>
            <div class="rst-status-box rst-status-good">🟢 Status: Active via Torn PDA</div>
        ` : `
            <div class="rst-prof-row">
                <span class="rst-prof-label">API Key:</span>
                <input type="text" id="rst-api-input-${this.storageKey}" class="rst-api-input ${hasKey ? 'rst-blur' : ''}" placeholder="Paste 16-char key..." spellcheck="false" autocomplete="off" maxlength="16" value="${savedKey}">
            </div>
            <div class="rst-prof-row" style="margin-bottom: 5px;">
                <span class="rst-prof-label"></span>
                <button class="rst-btn rst-btn-verify" id="rst-btn-verify-${this.storageKey}">VERIFY & SAVE</button>
                <button class="rst-btn rst-btn-clear" id="rst-btn-clear-${this.storageKey}">CLEAR</button>
            </div>
            <div id="rst-prof-status-${this.storageKey}" class="rst-status-box ${hasKey ? 'rst-status-good' : 'rst-status-bad'}">
                ${hasKey ? '🟢 Status: Key Verified & Active' : '🔴 Status: No Key Found'}
            </div>
        `;

        block.innerHTML = `<div class="rst-script-title">⚙️ ${this.scriptName}</div>${uiContent}`;
        body.appendChild(block);

        // Auto-open and scroll if a key is missing or Focus was requested
        if (shouldFocus || (!hasKey && !this.isPDA)) {
            body.style.display = 'block';
            document.getElementById('rst-prof-icon').innerHTML = '▼';
            setTimeout(() => {
                accordion.scrollIntoView({ behavior: 'smooth', block: 'center' });
                block.style.background = 'rgba(255, 170, 0, 0.1)';
                setTimeout(() => block.style.background = 'transparent', 2000);
            }, 300);
        }

        // Bind events if not PDA
        if (!this.isPDA) this.bindEvents();
    }

    bindEvents() {
        const input = document.getElementById(`rst-api-input-${this.storageKey}`);
        const verifyBtn = document.getElementById(`rst-btn-verify-${this.storageKey}`);
        const clearBtn = document.getElementById(`rst-btn-clear-${this.storageKey}`);
        const statusBox = document.getElementById(`rst-prof-status-${this.storageKey}`);

        input.addEventListener('blur', () => {
            if (input.value.length > 0) input.classList.add('rst-blur');
        });

        verifyBtn.addEventListener('click', () => {
            const val = input.value.trim();
            if (val.length === 16) {
                statusBox.className = 'rst-status-box rst-status-wait';
                statusBox.innerHTML = '⏳ Verifying with Torn API...';
                verifyBtn.disabled = true;

                GM_xmlhttpRequest({
                    method: "GET",
                    url: `https://api.torn.com/user/?selections=profile&key=${val}`,
                    onload: (response) => {
                        verifyBtn.disabled = false;
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.error) {
                                statusBox.className = 'rst-status-box rst-status-bad';
                                statusBox.innerHTML = `⚠️ Error: ${data.error.error}`;
                            } else {
                                GM_setValue(this.storageKey, val);
                                input.classList.add('rst-blur');
                                statusBox.className = 'rst-status-box rst-status-good';
                                statusBox.innerHTML = `🟢 Welcome, ${data.name}! Key Saved.`;
                                const banner = document.getElementById('rst-global-banner');
                                if (banner) banner.remove();
                            }
                        } catch (e) {
                            statusBox.className = 'rst-status-box rst-status-bad';
                            statusBox.innerHTML = '⚠️ Error parsing response.';
                        }
                    },
                    onerror: () => {
                        verifyBtn.disabled = false;
                        statusBox.className = 'rst-status-box rst-status-bad';
                        statusBox.innerHTML = '⚠️ Network error.';
                    }
                });
            } else {
                statusBox.className = 'rst-status-box rst-status-bad';
                statusBox.innerHTML = '⚠️ Key must be exactly 16 chars!';
            }
        });

        clearBtn.addEventListener('click', () => {
            GM_setValue(this.storageKey, '');
            input.value = '';
            input.classList.remove('rst-blur');
            statusBox.className = 'rst-status-box rst-status-bad';
            statusBox.innerHTML = '🔴 Key Cleared.';
        });
    }
}