Faction Revive Assistant

Checks all factions users in the hospital, and determines if they are revivable.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Faction Revive Assistant
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Checks all factions users in the hospital, and determines if they are revivable.
// @author       Marzen [3385879]
// @match        https://www.torn.com/factions.php?step=profile*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// ==/UserScript==

(function () {
    'use strict';

    // **CONFIGURABLE OPTIONS**
    const API_DELAY = 750;              // Set rate limit delay for API (in ms)
    const CONTINOUS_DELAY = 5000        // Set  delay for continous check button (in ms)

    // Variables for script
    let isRunning = false;
    let isContinuous = false;
    let isCanceled = false;

    function createApiKeyDiv() {
        let existingContainer = document.getElementById('revive-api-container');
        if (existingContainer) return;

        waitForMembersList((membersList) => {
            let container = document.createElement('div');
            container.id = 'revive-api-container';
            container.classList.add('revive-api-container');

            container.innerHTML = `
                <div class="revive-header">
                    Faction Revive Assistant
                </div>
                <div class="revive-input-group">
                    <label class="revive-label">API Key:</label>
                    <div class="revive-input-wrapper">
                        <input type="password" id="apiKeyInput" class="revive-input" value="${localStorage.reviveApiKey || ''}" />
                        <button id="toggleApiKeyVisibility" class="toggle-visibility-btn">👁</button>
                    </div>
                    <button id="updateKey" class="revive-asst-btn">Save</button>
                    <button id="clearKey" class="revive-asst-btn">Clear</button>
                </div>
                <div class="revive-buttons">
                    <button id="toggleScan" class="revive-asst-btn">Start Continuous Scan</button>
                    <button id="initiateReviveCheck" class="revive-asst-btn">Start Revive Check</button>
                </div>
            `;

            // Insert container before members list
            membersList.insertAdjacentElement('beforebegin', container);

            // Create styles
            const style = document.createElement('style');
            style.innerHTML = `
                .revive-api-container {
                    background: #222;
                    color: white;
                    padding: 10px;
                    border-radius: 5px;
                    margin-bottom: 10px;
                    display: flex;
                    flex-direction: column;
                    align-items: flex-start;
                    font-size: 14px;
                    margin-top: 10px;
                }

                .revive-header {
                    text-align: center;
                    font-size: 16px;
                    font-weight: bold;
                    margin-bottom: 8px;
                    padding-bottom: 8px;
                    border-bottom: 1px solid gray;
                    width: 100%;
                }

                .revive-input-group {
                    display: flex;
                    align-items: center;
                    gap: 6px;
                    width: 100%;
                    flex-wrap: nowrap;
                }

                .revive-label {
                    font-weight: bold;
                    white-space: nowrap;
                }

                .revive-input-wrapper {
                    position: relative;
                    flex-grow: 1;
                    min-width: 150px;
                    display: flex;
                    align-items: center;
                }

                .revive-input {
                    flex-grow: 1;
                    flex-shrink: 1;
                    min-width: 100px;
                    padding: 4px 30px 4px 8px;
                    text-align: left;
                    border-radius: 5px;
                    border: 1px solid #ccc;
                    background: #333;
                    color: white;
                }

                .toggle-visibility-btn {
                    position: absolute;
                    right: 4px;
                    background: transparent;
                    border: none;
                    cursor: pointer;
                    color: white;
                    font-size: 16px;
                }

                .toggle-visibility-btn:hover {
                    color: #aaa;
                }

                .revive-asst-btn {
                    background: #444;
                    color: white;
                    border: 1px solid #555;
                    padding: 6px 12px;
                    border-radius: 5px;
                    cursor: pointer;
                    transition: 0.2s;
                    white-space: nowrap;
                    text-align: center;
                }

                .revive-asst-btn:hover {
                    background: #555;
                    border-color: #777;
                }
                .revive-asst-btn:disabled {
                    background: #333 !important;
                    border-color: #222 !important;
                    color: gray !important;
                    cursor: not-allowed !important;
                }

                .revive-buttons .revive-asst-btn {
                    flex: 1;
                }

                .revive-input-group .revive-asst-btn {
                    flex: 0 0 auto;
                }

                .revive-buttons {
                    margin-top: 8px;
                    display: flex;
                    gap: 6px;
                    flex-wrap: wrap;
                    width: 100%;
                }

                @media (max-width: 600px) {
                    .revive-asst-btn, .revive-input {
                        width: 100%;
                    }

                    .revive-input-group .revive-asst-btn {
                        width: auto;
                    }
                }
            `;
            document.head.appendChild(style);

            // Button event handlers
            document.getElementById("updateKey").addEventListener("click", async () => {
                let apiKey = document.getElementById("apiKeyInput").value.trim();
                let isValid = await validateApiKey(apiKey);
                if (isValid) {
                    localStorage.reviveApiKey = apiKey;
                    alert('API key has been saved!')
                } else {
                    alert('API key validation failed. Please check the API key to ensure it is still active.')
                }
            });

            document.getElementById("clearKey").addEventListener("click", () => {
                localStorage.reviveApiKey = "";
                document.getElementById("apiKeyInput").value = "";
                alert('API key has been cleared!')
            });

            document.getElementById("toggleScan").addEventListener("click", function () {
                isContinuous = !isContinuous;
                if (isContinuous) {
                    isCanceled = false;
                } else {
                    isCanceled = true;
                }
                this.textContent = isContinuous ? "Stop Continuous Scan" : "Start Continuous Scan";
                document.getElementById("initiateReviveCheck").disabled = isContinuous;
                console.log(`[FRA] Continuous scan: ${isContinuous}`);
                if (isContinuous) updateFactionMembers();
            });

            document.getElementById("initiateReviveCheck").addEventListener("click", function () {
                if (!isRunning) {
                    isCanceled = false;
                    console.log("[FRA] Starting one-time revive check...");
                    this.textContent = "Cancel Revive Check";
                    document.getElementById("toggleScan").disabled = true;
                    updateFactionMembers().finally(() => {
                        this.textContent = "Start Revive Check";
                        document.getElementById("toggleScan").disabled = false;
                        console.log("[FRA] One-time revive check complete.");
                    });
                } else {
                    isCanceled = true;
                    this.textContent = "Start Revive Check";
                }
            });

            document.getElementById("toggleApiKeyVisibility").addEventListener("click", function () {
                let input = document.getElementById("apiKeyInput");
                input.type = input.type === "password" ? "text" : "password";
            });
        });
    }

    // Get user data
    async function queryUserData(userId, key) {
        try {
            const response = await fetch(`https://api.torn.com/v2/user/${userId}/`, {
                headers: {
                    "Authorization": `ApiKey ${key}`
                }
            });
            if (!response.ok) throw new Error("Unable to query user");
            return await response.json();
        } catch (error) {
            console.error("Failed to validate API key:", error);

            // Update progress indicator
            updateProgressIndicator("Invalid API key. Please check key and try again.")

            // Prevent script from running
            isRunning = false;
            isContinuous = false;

            // Re-enable buttons
            document.getElementById("toggleScan").disabled = false;
            document.getElementById("initiateReviveCheck").disabled = false;

            // Return false
            return false;
        }
    }

    // Scrape faction page for users in hospital
    async function updateFactionMembers(key) {
        if (isRunning) return;

        // Variables to ensure function isn't trigger by observer while already running
        isRunning = true;
        isCanceled = false;

        // Parse all rows to get faction members available
        let rows = document.querySelectorAll(".members-list .table-body .table-row");
        if (!rows.length) return (isRunning = false);

        // Get total faction members reported
        let totalMembers = getTotalFactionMembers();

        // Check if all faction members have loaded. If not, then exit script
        if (rows.length < totalMembers) return (isRunning = false);

        // Filter rows to exclude those hidden by TornTools
        rows = Array.from(rows).filter(row => !row.classList.contains('tt-hidden'));
        if (!rows.length) return (isRunning = false);

        // Verify API key after table has fully loaded
        const apiKey = localStorage.reviveApiKey;
        if (!apiKey) {
            console.warn("[FRA] No API Key found!");
            isRunning = false;
            updateProgressIndicator(`Revive check canceled.`);
            alert('API key is missing. Please enter the key and try again.');
            return;
        }

        // Variables for progress box
        let processed = 0, revivable = 0;
        let apiRequestCount = 0;
        const total = rows.length;

        // Update progress indicator
        updateProgressIndicator(`Processing faction members...`);

        // Track script run time for rate-limiting
        let runTime = Date.now();

        for (const row of rows) {
            if (isCanceled) {
                updateProgressIndicator("Revive check canceled.");
                isRunning = false;
                return;
            }

            const profileLink = row.querySelector('a[href*="/profiles.php?XID="]');
            const status = row.querySelector(".status span").textContent.trim();
            if (profileLink && status === "Hospital") {

                // Query user information and add indicator if they are in the hospital
                const match = profileLink.href.match(/XID=(\d+)/);
                if (match) {
                    let userId = match[1];

                    // Calculate expected elapsed time
                    const expectedElapsedTime = API_DELAY * apiRequestCount;
                    const actualElapsedTime = Date.now() - runTime;

                    // Delay if necessary
                    if (actualElapsedTime < expectedElapsedTime) {
                        await new Promise(resolve => setTimeout(resolve, expectedElapsedTime - actualElapsedTime));
                    }

                    let userData = await queryUserData(userId, apiKey);
                    if (userData) {
                        let isRevivable = userData?.profile?.revivable; // Check if user is revivable

                        // Get current user div
                        let userDiv = row.querySelector('[class^="userInfoBox"]');
                        if (userDiv) {
                            let reviveIcon = userDiv.querySelector(".revivable-indicator");

                            // Symbol to denote revive icon
                            let newText = isRevivable ? "✅" : "❌";

                            if (!reviveIcon) {
                                // If the indicator doesn't exist, create it
                                reviveIcon = document.createElement("span");
                                reviveIcon.className = "revivable-indicator";
                                reviveIcon.textContent = newText;
                                userDiv.insertBefore(reviveIcon, userDiv.firstChild);
                                console.log(`[FRA] Marked User ${userId} as ${isRevivable ? "revivable ✅" : "not revivable ❌"}.`);
                            } else {
                                // If the indicator exists, update it if needed
                                if (reviveIcon.textContent !== newText) {
                                    reviveIcon.textContent = newText;
                                    console.log(`[FRA] Updated User ${userId} to ${isRevivable ? "revivable ✅" : "not revivable ❌"}.`);
                                }
                            }
                        }

                        // Update revivable counter
                        if (isRevivable) revivable++;

                    } else {
                        console.log(`[FRA] Unable to query data for user: ${userId}`);

                    }

                    // Increment API request count
                    apiRequestCount++
                }
            }

            // Update progress indicator
            processed++;
            updateProgressIndicator(`Progress: ${total > 0 ? (processed / total * 100).toFixed(0) : 0}% | Revivable: ${revivable}`);
        }

        updateProgressIndicator(`Revivable: ${revivable}`);
        isRunning = false;

        // If continous scan is enabled, then reinitate after delay
        if (isContinuous) {
            console.log(`[FRA] Continuous scan enabled. Starting in ${CONTINOUS_DELAY} ms.`);
            setTimeout(updateFactionMembers, CONTINOUS_DELAY);
        } else {
            // Update buttons since run has finished
            document.getElementById("toggleScan").disabled = false;
            document.getElementById("initiateReviveCheck").disabled = false;
            console.log("[FRA] Revive check has been completed.");
        }
    }

    // Get the faction member count from faction info section
    function getTotalFactionMembers() {
        const memberText = document.querySelector(".f-info li:nth-child(3)")?.textContent.trim();
        if (!memberText) return 0;

        const memberCount = memberText.match(/(\d+)\s*\/\s*\d+/);
        return memberCount ? parseInt(memberCount[1], 10) : 0;
    }

    // Get number of members currently loaded in the table
    function getLoadedFactionMembers() {
        return document.querySelectorAll(".members-list .table-body .table-row").length;
    }

    // Function to wait for members list (using setInterval for ease of use)
    function waitForMembersList(callback) {
        console.log("[FRA] Waiting for members list...");
        const checkInterval = setInterval(() => {
            const membersList = document.querySelector('.members-list');
            if (membersList) {
                console.log("[FRA] Members list found!");
                clearInterval(checkInterval);
                callback(membersList);
            }
        }, 500);
    }

    // Progress indicator function
    function updateProgressIndicator(text) {
        let progressDiv = document.getElementById("revive-progress");

        if (!progressDiv) {
            // Create a new progress box if it doesn’t exist
            progressDiv = document.createElement("div");
            progressDiv.id = "revive-progress";
            progressDiv.style.position = "fixed";
            progressDiv.style.bottom = "10px";
            progressDiv.style.left = "10px";
            progressDiv.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
            progressDiv.style.color = "white";
            progressDiv.style.padding = "10px";
            progressDiv.style.borderRadius = "5px";
            progressDiv.style.zIndex = "1000";
            progressDiv.style.cursor = "pointer";
            progressDiv.onclick = () => progressDiv.remove();
            document.body.appendChild(progressDiv);
        }

        // Update text content
        progressDiv.textContent = text;
    }

    // Validate an API key
    async function validateApiKey(key) {
        try {
            const response = await fetch(`https://api.torn.com/v2/user/`, {
                headers: {
                    "Authorization": `ApiKey ${key}`
                }
            });

            // Get JSON from response
            const data = await response.json();
            if (data.error) {
                console.warn(`API Key validation failed: ${data.error.error}`);
                localStorage.reviveApiKey = "";
                return false;
            }
            return true;
        } catch (error) {
            console.error("Failed to validate API key:", error);
            return false;
        }
    }

    // Create div to allow for functionality
    createApiKeyDiv();

})();