OC 2.0 Helper

Displays faction members that are not currently participating in a faction crime. Highlights members inactive for days.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         OC 2.0 Helper
// @namespace    https://www.torn.com/
// @version      0.10.0
// @description  Displays faction members that are not currently participating in a faction crime. Highlights members inactive for days.
// @author       Slaterz [2479416]
// @match        https://www.torn.com/factions.php*
// @grant        GM_xmlhttpRequest
// @license      MIT

// ==/UserScript==
console.log("OC2 Helper script loading");
(function () {
  "use strict";

  const TORN_API_BASE_V2 = "https://api.torn.com/v2/";
  const TORN_API_BASE_V1 = "https://api.torn.com/";

  let {
    MEMBERS_ENDPOINT,
    CRIME_RANKS_ENDPOINT,
    PLANNED_CRIMES_ENDPOINT,
    RECRUITING_CRIMES_ENDPOINT,
  } = buildEndpoints();

  function constructEndpoint(base, params) {
    const apiKey = localStorage.getItem("OC2H_API");
    return `${base}?key=${apiKey}&${params}`;
  }

  function buildEndpoints() {
    return {
      MEMBERS_ENDPOINT: constructEndpoint(
        `${TORN_API_BASE_V2}faction/members`,
        "striptags=false"
      ),
      CRIME_RANKS_ENDPOINT: constructEndpoint(
        `${TORN_API_BASE_V1}faction/`,
        "selections=crimeexp"
      ),
      PLANNED_CRIMES_ENDPOINT: constructEndpoint(
        `${TORN_API_BASE_V2}faction/crimes`,
        "cat=planning&offset=0"
      ),
      RECRUITING_CRIMES_ENDPOINT: constructEndpoint(
        `${TORN_API_BASE_V2}faction/crimes`,
        "cat=recruiting&offset=0"
      ),
    };
  }

  async function fetchData(endpoint) {
    try {
      const response = await fetch(endpoint);
      if (!response.ok) {
        throw new Error(`API call failed with status ${response.status}`);
      }

      // Parse the response JSON
      const data = await response.json();

      // Check for API error in the response
      if (data.error) {
        throw new Error(data.error.error); // Extract and throw the API error message
      }

      return data; // Return the valid data
    } catch (error) {
      console.error("Error fetching data from API:", error.message); // Log the error message
      throw error; // Re-throw the error to propagate it to the caller
    }
  }

  function populateFactionMembersTable(members, recruitingCrimeMembers) {
    const table = document.getElementById("oc2h-faction-members-table");
    const tbody = table.querySelector("tbody");
    tbody.innerHTML = ""; // Clear previous rows

    members.forEach((member) => {
      const row = document.createElement("tr");
      const lastActiveText = member.last_action?.relative || "Unknown";

      // Check if the member is inactive
      if (lastActiveText.includes("day")) {
        row.classList.add("inactive-row"); // Apply class to the entire row
      }

      // Check if the member is in a recruiting crime
      if (recruitingCrimeMembers.has(member.id.toString())) {
        row.classList.add("recruiting-row");
      }

      row.innerHTML = `
      <td class="member-name">${member.name} [${member.id}]</td>
      <td>${member.rank || "Unranked"}</td>
      <td>${lastActiveText}</td>
    `;
      tbody.appendChild(row);
    });

    table.classList.remove("hidden");
  }

  async function fetchAllFactionData() {
    const dataContainer = document.getElementById("oc2h-data-container");
    const errorMessage = document.getElementById("oc2h-error-message");

    try {
      const [membersData, crimeRanks, plannedCrimes, recruitingCrimes] =
        await Promise.all([
          fetchData(MEMBERS_ENDPOINT),
          fetchData(CRIME_RANKS_ENDPOINT),
          fetchData(PLANNED_CRIMES_ENDPOINT),
          fetchData(RECRUITING_CRIMES_ENDPOINT),
        ]);

      if (!membersData || !crimeRanks || !plannedCrimes || !recruitingCrimes) {
        throw new Error("Failed to fetch faction data.");
      }

      const membersObj = membersData.members || {};
      const crimeRanksArray = crimeRanks.crimeexp || {};

      // Create a Set of members already in planned crimes
      const plannedCrimeMembers = new Set();
      plannedCrimes.crimes.forEach((crime) => {
        crime.slots.forEach((slot) => {
          if (slot.user && slot.user.id) {
            plannedCrimeMembers.add(slot.user.id.toString());
          }
        });
      });

      //Create a Set of members in recruiting crimes
      const recruitingCrimeMembers = new Set();
      recruitingCrimes.crimes.forEach((crime) => {
        crime.slots.forEach((slot) => {
          if (slot.user && slot.user.id) {
            recruitingCrimeMembers.add(slot.user.id.toString());
          }
        });
      });

      // Map crime ranks to IDs
      const crimeRankMap = {};
      crimeRanksArray.forEach((id, index) => {
        crimeRankMap[id.toString()] = index + 1;
      });

      // Filter and sort members, and add the rank property
      const sortedMembers = Object.values(membersObj)
        .filter((member) => !plannedCrimeMembers.has(member.id.toString())) // Exclude planned members
        .map((member) => ({
          ...member,
          rank: crimeRankMap[member.id.toString()] || "Unranked", // Map the rank
        }))
        .sort((a, b) => {
          const rankA = crimeRankMap[a.id.toString()] || Infinity;
          const rankB = crimeRankMap[b.id.toString()] || Infinity;
          return rankA - rankB;
        });

      if (sortedMembers.length === 0) {
        throw new Error("No valid faction members found.");
      }

      // Populate table and show the data container
      populateFactionMembersTable(sortedMembers, recruitingCrimeMembers);
      dataContainer.classList.remove("hidden");
      errorMessage.classList.add("hidden");
    } catch (error) {
      // Display detailed error message
      console.error("Error fetching faction data:", error.message);
      errorMessage.textContent =
        error.message || "An unexpected error occurred.";
      errorMessage.classList.remove("hidden");
      dataContainer.classList.add("hidden");
    }
  }

  let uiInitialized = false; // Flag to track if the UI has been created

  // Function to create and insert the OC2 Helper UI.
  function createOC2HelperUI() {
    const factionCrimesElement = document.getElementById("faction-crimes");

    if (!factionCrimesElement) {
      return; // Do nothing if the target element doesn't exist
    }

    if (document.getElementById("oc2h-helper-container")) {
      return; // Skip if the UI already exists
    }

    const factionCrimesWrap = factionCrimesElement.querySelector(
      ".faction-crimes-wrap"
    );

    if (!factionCrimesWrap) {
      return; // Do nothing if the reference element doesn't exist
    }

    // Create the OC2 Helper container
    const oc2HelperContainer = document.createElement("div");
    oc2HelperContainer.id = "oc2h-helper-container";
    oc2HelperContainer.className = "oc2h-container";
    oc2HelperContainer.innerHTML = `
    <div class="oc2h-header">
      <div class="oc2h-title">OC 2.0 Helper</div>
      <div id="oc2h-api-key-wrapper" class="oc2h-api-key-wrapper">
        <div id="oc2h-api-key-form" class="oc2h-api-key-form hidden">
          <input
            type="text"
            id="oc2h-api-key-input"
            class="oc2h-api-key-input"
            placeholder="Enter Limited API key"
          />
          <button id="oc2h-api-key-submit" class="torn-btn oc2h-api-key-submit">Submit</button>
        </div>
        <button id="oc2h-set-api-key-btn" class="torn-btn oc2h-set-api-key-btn">Set API Key</button>
      </div>
    </div>
    <div id="oc2h-data-container" class="oc2h-data-container hidden">
      <div class="oc2h-table-title">Members not in Planning OC</div>
      <table id="oc2h-faction-members-table" class="oc2h-table hidden">
        <thead>
          <tr>
            <th>Member</th>
            <th>Rank</th>
            <th>Last Active</th>
          </tr>
        </thead>
        <tbody>
          <!-- Faction members will be populated here -->
        </tbody>
      </table>
    </div>
    <div id="oc2h-error-message" class="oc2h-error-message hidden">
      <!-- Error messages will be displayed here -->
    </div>
  `;

    factionCrimesElement.insertBefore(oc2HelperContainer, factionCrimesWrap);

    // Add event listeners for the API key form and fetch button
    initializeApiKeyForm();

    // Automatically fetch data
    fetchAllFactionData();
  }

  function initializeApiKeyForm() {
    const setApiKeyBtn = document.getElementById("oc2h-set-api-key-btn");
    setApiKeyBtn.addEventListener("click", () => {
      const form = document.getElementById("oc2h-api-key-form");
      form.classList.remove("hidden"); // Show the input box and submit button
    });

    const submitBtn = document.getElementById("oc2h-api-key-submit");
    submitBtn.addEventListener("click", async () => {
      const input = document.getElementById("oc2h-api-key-input");
      const newKey = input.value.trim();
      console.log("New API Key entered:", newKey); // Log for debugging
      if (newKey) {
        localStorage.setItem("OC2H_API", newKey); // Directly store the key in localStorage
        alert("API Key has been set!");
        input.value = ""; // Clear the input field
        const form = document.getElementById("oc2h-api-key-form");
        form.classList.add("hidden"); // Hide the input box and submit button
        console.log("API Key successfully updated in localStorage.");
        // Rebuild the endpoints with the new API key
        ({ MEMBERS_ENDPOINT, CRIME_RANKS_ENDPOINT, PLANNED_CRIMES_ENDPOINT } =
          buildEndpoints());
        console.log("Endpoints rebuilt with new API key.");

        // Trigger fetching data
        await fetchAllFactionData();
      } else {
        alert("Please enter a valid API key.");
      }
    });
  }

  // Function to observe DOM changes and reinitialize UI
  function observeCrimesTab() {
    const observer = new MutationObserver(() => {
      const factionCrimesElement = document.getElementById("faction-crimes");

      if (factionCrimesElement) {
        createOC2HelperUI();
      } else {
        uiInitialized = false; // Reset the flag when the element is removed
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  // Add global styles
  function addGlobalStyle(css) {
    const head = document.getElementsByTagName("head")[0];
    if (!head) return;

    const style = document.createElement("style");
    style.type = "text/css";
    style.innerHTML = css;
    head.appendChild(style);
  }

  addGlobalStyle(`
    .oc2h-container {
        background: var(--default-bg-panel-color);
        padding: 15px;
        margin: 15px 0;
        border: 1px solid var(--border-color);
        border-radius: 5px;
        box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
    }
    .oc2h-header {
        display: flex;
        justify-content: space-between;
        align-items: center; /* Ensures vertical alignment */
        margin-bottom: 10px;
    }
    .oc2h-title {
        font-size: 2.5em;
        font-weight: bold;
        margin: 0; /* Remove margin to align with the header */
        color: var(--text-color);
    }
    .oc2h-table-title {
        font-size: 2.0em;
        font-weight: bold;
        margin-top: 10px;
        margin-bottom: 10px;
        color: var(--text-color);
    }
    .oc2h-table {
        width: 100%;
        border-collapse: collapse;
        margin-top: 10px;
        font-size: 1.1em;
        background-color: var(--default-bg-panel-color);
    }
    .oc2h-table thead th {
        background: var(--btn-background);
        color: var(--btn-color);
        text-align: left;
        padding: 12px;
        border-bottom: 2px solid var(--border-color);
        text-transform: uppercase;
        font-size: 1.2em;
        font-weight: bold;
    }
    .oc2h-table tbody td {
        padding: 10px;
        border-bottom: 1px solid var(--border-color);
        color: var(--text-color);
    }
    .oc2h-table tbody tr td {
      padding: 2px 10px;
    }
    .oc2h-table tbody tr:hover {
        background-color: var(--btn-hover-background);
    }
    .oc2h-table tbody tr:nth-child(odd) {
        background-color: rgba(0, 0, 0, 0.05); /* Subtle striped rows for readability */
    }
    .oc2h-table tbody tr.inactive-member {
        background-color: rgba(255, 0, 0, 0.1); /* Highlight inactive members */
    }
    .oc2h-table td.member-name {
        font-weight: bold;
        color: var(--text-color);
    }
    .oc2h-api-key-wrapper {
        display: flex;
        align-items: center;
        gap: 10px; /* Add spacing between the elements */
    }
    .oc2h-api-key-form.hidden {
        display: none; /* Hides the API key form */
    }
    .oc2h-api-key-form {
        display: flex;
        align-items: center;
        gap: 5px;
    }
    .oc2h-api-key-input {
        padding: 5px;
        border: 1px solid var(--border-color);
        border-radius: 4px;
        font-size: 0.9em;
    }
    .oc2h-api-key-submit {
        padding: 5px 10px;
        background: var(--btn-background);
        color: var(--btn-color);
        border: none;
        border-radius: 4px;
        cursor: pointer;
    }
    .oc2h-api-key-submit:hover {
        background: var(--btn-hover-background);
    }
    .oc2h-set-api-key-btn {
        background: var(--btn-background);
        color: var(--btn-color);
        border: 1px solid var(--btn-border-color);
        padding: 5px 10px;
        font-size: 0.9em;
        cursor: pointer;
        border-radius: 4px;
    }
    .oc2h-set-api-key-btn:hover {
        background: var(--btn-hover-background);
    }
    .oc2h-data-container.hidden {
        display: none;
    }
    .oc2h-error-message {
        margin-top: 15px;
        padding: 10px;
        background: rgba(255, 0, 0, 0.1);
        border: 1px solid rgba(255, 0, 0, 0.3);
        color: var(--oc-level-color-text-hard);
        font-weight: bold;
        border-radius: 5px;
    }
    .oc2h-error-message.hidden {
        display: none;
    }
    .oc2h-table tbody tr.inactive-row {
        color: var(--oc-level-color-text-hard);
        font-weight: bold;
    }
    .oc2h-table tbody tr.recruiting-row {
        color: var(--oc-slot-menu-text);
        font-weight: bold;
    }
  `);

  // Initialize the script
  function waitForDOMReady() {
    const interval = setInterval(() => {
      if (document.body) {
        clearInterval(interval);
        observeCrimesTab();
        createOC2HelperUI();
      }
    }, 100);
  }

  waitForDOMReady();
})();