OC 2.0 Helper

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

  1. // ==UserScript==
  2. // @name OC 2.0 Helper
  3. // @namespace https://www.torn.com/
  4. // @version 0.10.0
  5. // @description Displays faction members that are not currently participating in a faction crime. Highlights members inactive for days.
  6. // @author Slaterz [2479416]
  7. // @match https://www.torn.com/factions.php*
  8. // @grant GM_xmlhttpRequest
  9. // @license MIT
  10.  
  11. // ==/UserScript==
  12. console.log("OC2 Helper script loading");
  13. (function () {
  14. "use strict";
  15.  
  16. const TORN_API_BASE_V2 = "https://api.torn.com/v2/";
  17. const TORN_API_BASE_V1 = "https://api.torn.com/";
  18.  
  19. let {
  20. MEMBERS_ENDPOINT,
  21. CRIME_RANKS_ENDPOINT,
  22. PLANNED_CRIMES_ENDPOINT,
  23. RECRUITING_CRIMES_ENDPOINT,
  24. } = buildEndpoints();
  25.  
  26. function constructEndpoint(base, params) {
  27. const apiKey = localStorage.getItem("OC2H_API");
  28. return `${base}?key=${apiKey}&${params}`;
  29. }
  30.  
  31. function buildEndpoints() {
  32. return {
  33. MEMBERS_ENDPOINT: constructEndpoint(
  34. `${TORN_API_BASE_V2}faction/members`,
  35. "striptags=false"
  36. ),
  37. CRIME_RANKS_ENDPOINT: constructEndpoint(
  38. `${TORN_API_BASE_V1}faction/`,
  39. "selections=crimeexp"
  40. ),
  41. PLANNED_CRIMES_ENDPOINT: constructEndpoint(
  42. `${TORN_API_BASE_V2}faction/crimes`,
  43. "cat=planning&offset=0"
  44. ),
  45. RECRUITING_CRIMES_ENDPOINT: constructEndpoint(
  46. `${TORN_API_BASE_V2}faction/crimes`,
  47. "cat=recruiting&offset=0"
  48. ),
  49. };
  50. }
  51.  
  52. async function fetchData(endpoint) {
  53. try {
  54. const response = await fetch(endpoint);
  55. if (!response.ok) {
  56. throw new Error(`API call failed with status ${response.status}`);
  57. }
  58.  
  59. // Parse the response JSON
  60. const data = await response.json();
  61.  
  62. // Check for API error in the response
  63. if (data.error) {
  64. throw new Error(data.error.error); // Extract and throw the API error message
  65. }
  66.  
  67. return data; // Return the valid data
  68. } catch (error) {
  69. console.error("Error fetching data from API:", error.message); // Log the error message
  70. throw error; // Re-throw the error to propagate it to the caller
  71. }
  72. }
  73.  
  74. function populateFactionMembersTable(members, recruitingCrimeMembers) {
  75. const table = document.getElementById("oc2h-faction-members-table");
  76. const tbody = table.querySelector("tbody");
  77. tbody.innerHTML = ""; // Clear previous rows
  78.  
  79. members.forEach((member) => {
  80. const row = document.createElement("tr");
  81. const lastActiveText = member.last_action?.relative || "Unknown";
  82.  
  83. // Check if the member is inactive
  84. if (lastActiveText.includes("day")) {
  85. row.classList.add("inactive-row"); // Apply class to the entire row
  86. }
  87.  
  88. // Check if the member is in a recruiting crime
  89. if (recruitingCrimeMembers.has(member.id.toString())) {
  90. row.classList.add("recruiting-row");
  91. }
  92.  
  93. row.innerHTML = `
  94. <td class="member-name">${member.name} [${member.id}]</td>
  95. <td>${member.rank || "Unranked"}</td>
  96. <td>${lastActiveText}</td>
  97. `;
  98. tbody.appendChild(row);
  99. });
  100.  
  101. table.classList.remove("hidden");
  102. }
  103.  
  104. async function fetchAllFactionData() {
  105. const dataContainer = document.getElementById("oc2h-data-container");
  106. const errorMessage = document.getElementById("oc2h-error-message");
  107.  
  108. try {
  109. const [membersData, crimeRanks, plannedCrimes, recruitingCrimes] =
  110. await Promise.all([
  111. fetchData(MEMBERS_ENDPOINT),
  112. fetchData(CRIME_RANKS_ENDPOINT),
  113. fetchData(PLANNED_CRIMES_ENDPOINT),
  114. fetchData(RECRUITING_CRIMES_ENDPOINT),
  115. ]);
  116.  
  117. if (!membersData || !crimeRanks || !plannedCrimes || !recruitingCrimes) {
  118. throw new Error("Failed to fetch faction data.");
  119. }
  120.  
  121. const membersObj = membersData.members || {};
  122. const crimeRanksArray = crimeRanks.crimeexp || {};
  123.  
  124. // Create a Set of members already in planned crimes
  125. const plannedCrimeMembers = new Set();
  126. plannedCrimes.crimes.forEach((crime) => {
  127. crime.slots.forEach((slot) => {
  128. if (slot.user && slot.user.id) {
  129. plannedCrimeMembers.add(slot.user.id.toString());
  130. }
  131. });
  132. });
  133.  
  134. //Create a Set of members in recruiting crimes
  135. const recruitingCrimeMembers = new Set();
  136. recruitingCrimes.crimes.forEach((crime) => {
  137. crime.slots.forEach((slot) => {
  138. if (slot.user && slot.user.id) {
  139. recruitingCrimeMembers.add(slot.user.id.toString());
  140. }
  141. });
  142. });
  143.  
  144. // Map crime ranks to IDs
  145. const crimeRankMap = {};
  146. crimeRanksArray.forEach((id, index) => {
  147. crimeRankMap[id.toString()] = index + 1;
  148. });
  149.  
  150. // Filter and sort members, and add the rank property
  151. const sortedMembers = Object.values(membersObj)
  152. .filter((member) => !plannedCrimeMembers.has(member.id.toString())) // Exclude planned members
  153. .map((member) => ({
  154. ...member,
  155. rank: crimeRankMap[member.id.toString()] || "Unranked", // Map the rank
  156. }))
  157. .sort((a, b) => {
  158. const rankA = crimeRankMap[a.id.toString()] || Infinity;
  159. const rankB = crimeRankMap[b.id.toString()] || Infinity;
  160. return rankA - rankB;
  161. });
  162.  
  163. if (sortedMembers.length === 0) {
  164. throw new Error("No valid faction members found.");
  165. }
  166.  
  167. // Populate table and show the data container
  168. populateFactionMembersTable(sortedMembers, recruitingCrimeMembers);
  169. dataContainer.classList.remove("hidden");
  170. errorMessage.classList.add("hidden");
  171. } catch (error) {
  172. // Display detailed error message
  173. console.error("Error fetching faction data:", error.message);
  174. errorMessage.textContent =
  175. error.message || "An unexpected error occurred.";
  176. errorMessage.classList.remove("hidden");
  177. dataContainer.classList.add("hidden");
  178. }
  179. }
  180.  
  181. let uiInitialized = false; // Flag to track if the UI has been created
  182.  
  183. // Function to create and insert the OC2 Helper UI.
  184. function createOC2HelperUI() {
  185. const factionCrimesElement = document.getElementById("faction-crimes");
  186.  
  187. if (!factionCrimesElement) {
  188. return; // Do nothing if the target element doesn't exist
  189. }
  190.  
  191. if (document.getElementById("oc2h-helper-container")) {
  192. return; // Skip if the UI already exists
  193. }
  194.  
  195. const factionCrimesWrap = factionCrimesElement.querySelector(
  196. ".faction-crimes-wrap"
  197. );
  198.  
  199. if (!factionCrimesWrap) {
  200. return; // Do nothing if the reference element doesn't exist
  201. }
  202.  
  203. // Create the OC2 Helper container
  204. const oc2HelperContainer = document.createElement("div");
  205. oc2HelperContainer.id = "oc2h-helper-container";
  206. oc2HelperContainer.className = "oc2h-container";
  207. oc2HelperContainer.innerHTML = `
  208. <div class="oc2h-header">
  209. <div class="oc2h-title">OC 2.0 Helper</div>
  210. <div id="oc2h-api-key-wrapper" class="oc2h-api-key-wrapper">
  211. <div id="oc2h-api-key-form" class="oc2h-api-key-form hidden">
  212. <input
  213. type="text"
  214. id="oc2h-api-key-input"
  215. class="oc2h-api-key-input"
  216. placeholder="Enter Limited API key"
  217. />
  218. <button id="oc2h-api-key-submit" class="torn-btn oc2h-api-key-submit">Submit</button>
  219. </div>
  220. <button id="oc2h-set-api-key-btn" class="torn-btn oc2h-set-api-key-btn">Set API Key</button>
  221. </div>
  222. </div>
  223. <div id="oc2h-data-container" class="oc2h-data-container hidden">
  224. <div class="oc2h-table-title">Members not in Planning OC</div>
  225. <table id="oc2h-faction-members-table" class="oc2h-table hidden">
  226. <thead>
  227. <tr>
  228. <th>Member</th>
  229. <th>Rank</th>
  230. <th>Last Active</th>
  231. </tr>
  232. </thead>
  233. <tbody>
  234. <!-- Faction members will be populated here -->
  235. </tbody>
  236. </table>
  237. </div>
  238. <div id="oc2h-error-message" class="oc2h-error-message hidden">
  239. <!-- Error messages will be displayed here -->
  240. </div>
  241. `;
  242.  
  243. factionCrimesElement.insertBefore(oc2HelperContainer, factionCrimesWrap);
  244.  
  245. // Add event listeners for the API key form and fetch button
  246. initializeApiKeyForm();
  247.  
  248. // Automatically fetch data
  249. fetchAllFactionData();
  250. }
  251.  
  252. function initializeApiKeyForm() {
  253. const setApiKeyBtn = document.getElementById("oc2h-set-api-key-btn");
  254. setApiKeyBtn.addEventListener("click", () => {
  255. const form = document.getElementById("oc2h-api-key-form");
  256. form.classList.remove("hidden"); // Show the input box and submit button
  257. });
  258.  
  259. const submitBtn = document.getElementById("oc2h-api-key-submit");
  260. submitBtn.addEventListener("click", async () => {
  261. const input = document.getElementById("oc2h-api-key-input");
  262. const newKey = input.value.trim();
  263. console.log("New API Key entered:", newKey); // Log for debugging
  264. if (newKey) {
  265. localStorage.setItem("OC2H_API", newKey); // Directly store the key in localStorage
  266. alert("API Key has been set!");
  267. input.value = ""; // Clear the input field
  268. const form = document.getElementById("oc2h-api-key-form");
  269. form.classList.add("hidden"); // Hide the input box and submit button
  270. console.log("API Key successfully updated in localStorage.");
  271. // Rebuild the endpoints with the new API key
  272. ({ MEMBERS_ENDPOINT, CRIME_RANKS_ENDPOINT, PLANNED_CRIMES_ENDPOINT } =
  273. buildEndpoints());
  274. console.log("Endpoints rebuilt with new API key.");
  275.  
  276. // Trigger fetching data
  277. await fetchAllFactionData();
  278. } else {
  279. alert("Please enter a valid API key.");
  280. }
  281. });
  282. }
  283.  
  284. // Function to observe DOM changes and reinitialize UI
  285. function observeCrimesTab() {
  286. const observer = new MutationObserver(() => {
  287. const factionCrimesElement = document.getElementById("faction-crimes");
  288.  
  289. if (factionCrimesElement) {
  290. createOC2HelperUI();
  291. } else {
  292. uiInitialized = false; // Reset the flag when the element is removed
  293. }
  294. });
  295.  
  296. observer.observe(document.body, {
  297. childList: true,
  298. subtree: true,
  299. });
  300. }
  301.  
  302. // Add global styles
  303. function addGlobalStyle(css) {
  304. const head = document.getElementsByTagName("head")[0];
  305. if (!head) return;
  306.  
  307. const style = document.createElement("style");
  308. style.type = "text/css";
  309. style.innerHTML = css;
  310. head.appendChild(style);
  311. }
  312.  
  313. addGlobalStyle(`
  314. .oc2h-container {
  315. background: var(--default-bg-panel-color);
  316. padding: 15px;
  317. margin: 15px 0;
  318. border: 1px solid var(--border-color);
  319. border-radius: 5px;
  320. box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
  321. }
  322. .oc2h-header {
  323. display: flex;
  324. justify-content: space-between;
  325. align-items: center; /* Ensures vertical alignment */
  326. margin-bottom: 10px;
  327. }
  328. .oc2h-title {
  329. font-size: 2.5em;
  330. font-weight: bold;
  331. margin: 0; /* Remove margin to align with the header */
  332. color: var(--text-color);
  333. }
  334. .oc2h-table-title {
  335. font-size: 2.0em;
  336. font-weight: bold;
  337. margin-top: 10px;
  338. margin-bottom: 10px;
  339. color: var(--text-color);
  340. }
  341. .oc2h-table {
  342. width: 100%;
  343. border-collapse: collapse;
  344. margin-top: 10px;
  345. font-size: 1.1em;
  346. background-color: var(--default-bg-panel-color);
  347. }
  348. .oc2h-table thead th {
  349. background: var(--btn-background);
  350. color: var(--btn-color);
  351. text-align: left;
  352. padding: 12px;
  353. border-bottom: 2px solid var(--border-color);
  354. text-transform: uppercase;
  355. font-size: 1.2em;
  356. font-weight: bold;
  357. }
  358. .oc2h-table tbody td {
  359. padding: 10px;
  360. border-bottom: 1px solid var(--border-color);
  361. color: var(--text-color);
  362. }
  363. .oc2h-table tbody tr td {
  364. padding: 2px 10px;
  365. }
  366. .oc2h-table tbody tr:hover {
  367. background-color: var(--btn-hover-background);
  368. }
  369. .oc2h-table tbody tr:nth-child(odd) {
  370. background-color: rgba(0, 0, 0, 0.05); /* Subtle striped rows for readability */
  371. }
  372. .oc2h-table tbody tr.inactive-member {
  373. background-color: rgba(255, 0, 0, 0.1); /* Highlight inactive members */
  374. }
  375. .oc2h-table td.member-name {
  376. font-weight: bold;
  377. color: var(--text-color);
  378. }
  379. .oc2h-api-key-wrapper {
  380. display: flex;
  381. align-items: center;
  382. gap: 10px; /* Add spacing between the elements */
  383. }
  384. .oc2h-api-key-form.hidden {
  385. display: none; /* Hides the API key form */
  386. }
  387. .oc2h-api-key-form {
  388. display: flex;
  389. align-items: center;
  390. gap: 5px;
  391. }
  392. .oc2h-api-key-input {
  393. padding: 5px;
  394. border: 1px solid var(--border-color);
  395. border-radius: 4px;
  396. font-size: 0.9em;
  397. }
  398. .oc2h-api-key-submit {
  399. padding: 5px 10px;
  400. background: var(--btn-background);
  401. color: var(--btn-color);
  402. border: none;
  403. border-radius: 4px;
  404. cursor: pointer;
  405. }
  406. .oc2h-api-key-submit:hover {
  407. background: var(--btn-hover-background);
  408. }
  409. .oc2h-set-api-key-btn {
  410. background: var(--btn-background);
  411. color: var(--btn-color);
  412. border: 1px solid var(--btn-border-color);
  413. padding: 5px 10px;
  414. font-size: 0.9em;
  415. cursor: pointer;
  416. border-radius: 4px;
  417. }
  418. .oc2h-set-api-key-btn:hover {
  419. background: var(--btn-hover-background);
  420. }
  421. .oc2h-data-container.hidden {
  422. display: none;
  423. }
  424. .oc2h-error-message {
  425. margin-top: 15px;
  426. padding: 10px;
  427. background: rgba(255, 0, 0, 0.1);
  428. border: 1px solid rgba(255, 0, 0, 0.3);
  429. color: var(--oc-level-color-text-hard);
  430. font-weight: bold;
  431. border-radius: 5px;
  432. }
  433. .oc2h-error-message.hidden {
  434. display: none;
  435. }
  436. .oc2h-table tbody tr.inactive-row {
  437. color: var(--oc-level-color-text-hard);
  438. font-weight: bold;
  439. }
  440. .oc2h-table tbody tr.recruiting-row {
  441. color: var(--oc-slot-menu-text);
  442. font-weight: bold;
  443. }
  444. `);
  445.  
  446. // Initialize the script
  447. function waitForDOMReady() {
  448. const interval = setInterval(() => {
  449. if (document.body) {
  450. clearInterval(interval);
  451. observeCrimesTab();
  452. createOC2HelperUI();
  453. }
  454. }, 100);
  455. }
  456.  
  457. waitForDOMReady();
  458. })();