// ==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();
})();