Faction Revive Assistant

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();

})();