[TORN] OC 2.0 Helper

Adds a list of members available for OC2.0, and adds a notifier to the sidebar if you are not in an OC.

目前为 2025-01-06 提交的版本。查看 最新版本

// ==UserScript==
// @name        [TORN] OC 2.0 Helper
// @namespace   Violentmonkey Scripts
// @match       https://www.torn.com/*
// @version     1.5
// @author      whatdoesthespacebardo / callmericky [3299880]
// @description Adds a list of members available for OC2.0, and adds a notifier to the sidebar if you are not in an OC.
// @grant       GM_registerMenuCommand
// @grant       GM.setValue
// @grant       GM.getValue
// @license     GNU GPLv3
// ==/UserScript==


/*
 * All edits can be done from the dropdown menu from your browser's userscript extension
 * If the dropdown menu doesn't work, you can still manually add your API key here
 */
var APIKey = "";

/* =============================================
 * STOP CHANGING THINGS FROM THIS POINT ONWARD
 =============================================== */
const PDA_APIKey = "###PDA-APIKEY###"
let memberInfo = {};
let pageURL = $(location).attr("href");
let totalMembers = 0;
let availableMembers = 0;
let activeMembers = 0;
let userInfo = {};

let _isWindowSmall = window.matchMedia("(max-width: 784px)")

if (!isPDA()) {
  const menu_command_1 = GM_registerMenuCommand("Set API Key (Limited + faction access)", menuCommand)
}
async function getAPIKey() {
  if (isPDA()) {
    APIKey = PDA_APIKey
    return PDA_APIKey
  } else {
    return await GM.getValue("CMR_OC2_APIKey", null)
      .then(function(data) {
        APIKey = data
        return data
      })
  }
}

function isPDA() {
  const PDATestRegex = !/^(###).+(###)$/.test(PDA_APIKey);
  return PDATestRegex;
}

function menuCommand(event) {
  let _userKey = window.prompt("OC 2.0 participation and notifier\n- Requires a minimal API key with faction access\n\nSet API Key:", APIKey)
  if ((_userKey == null || _userKey == "")) {
    window.alert("OC 2.0 participation and notifier\nYou did not set an API Key - this script will not run")
  } else {
    GM.setValue("CMR_OC2_APIKey", _userKey)
  }
}

function checkCrimesPage(pageURL) {
  if (pageURL.search("step=your") > 0 && pageURL.search("tab=crimes") > 0) {
    return true;
  } else {
    return false;
  }
}

function getUserID() {
  let _profileLink = $(".settings-menu > .link > a")[0]
  let _matchregex = /profiles\.php.+XID=(\d+)/i
  let _userID = _matchregex.exec(_profileLink)
  return _userID[1]
}


async function getFactionMembers() {
  return await $.getJSON(`https://api.torn.com/v2/faction/members?key=${APIKey}&striptags=true`)
   .then(function(data) {
      return data;
    });
}

async function getCrimesList() {
  return await $.getJSON(`https://api.torn.com/v2/faction/crimes?key=${APIKey}&cat=available&offset=0`)
   .then(function(data) {
      return data;
    });
}

function checkMembersInCrimes(_members, _crimes) {
  //put all member ids into a list
  for (i = 0; i < (_members.members).length; i++) {
    if (_members.members[i].status.state == "Fallen") {
      //skip fallen members
    } else if (_members.members[i].position == "Recruit") {
      //skip recruits since they can't join OC
    } else {
      memberInfo[_members.members[i].id] = {
        "name": _members.members[i].name,
        "last_action": _members.members[i].last_action.relative,
        "status": _members.members[i].status.description
      }
    }
  }
  totalMembers = (_members.members).length;
  activeMembers = 0 //if this doesn't reset, hashchange will cause the following part to re-fire and get activemembers count wrong.

  //go through crime list
  for (i = 0; i < (_crimes.crimes).length; i++) {
    //if crime is not initiated, it will be null for initiated_at. No point looking for members because there won't be any.
    if (_crimes.crimes[i].initiated_at) {
      for (j=0; j<(_crimes.crimes[i].slots).length; j++)
        if (_crimes.crimes[i].slots[j].user_id) {
          memberInfo[_crimes.crimes[i].slots[j].user_id].crimeInfo = {
            "crimeName": _crimes.crimes[i].name,
            "crimeDifficulty": _crimes.crimes[i].difficulty,
            "crimeId": _crimes.crimes[i].id,
            "crimePosition": _crimes.crimes[i].slots[j].position,
            "crimeSuccess": _crimes.crimes[i].slots[j].success_chance
          }
          activeMembers = activeMembers + 1;
          if ((_crimes.crimes[i].slots[j].user_id) == getUserID()) {
            userInfo = memberInfo[_crimes.crimes[i].slots[j].user_id]
          }
        }
    }
  }
  availableMembers = totalMembers - activeMembers;
}

function putMemberInfoIntoTable() {
  if ($(".OC2-memberTable .OC2-tablecell")[0]) {
    return
  }
  for (var _key of Object.keys(memberInfo)) {
    let _outputAvail = "AVAILABLE"
    let _outputHTML = ""
    let _memberHighlight = "OC2-memberAvailable"
    if (memberInfo[_key].crimeInfo) {
      _outputAvail = `${memberInfo[_key].crimeInfo.crimePosition} of <a href="https://www.torn.com/factions.php?step=your&type=5#/tab=crimes&crimeId=${memberInfo[_key].crimeInfo.crimeId}">${memberInfo[_key].crimeInfo.crimeName}</a> (Lv ${memberInfo[_key].crimeInfo.crimeDifficulty})`
      _memberHighlight = "OC2-memberInCrime"
    }
    _outputHTML = (`<li class="table-cell ${_memberHighlight}">
        <div class="OC2-tablecell OC2-tableMember">${memberInfo[_key].name} [${_key}]</div>
        <div class="OC2-tablecell OC2-tableAvailability">${_outputAvail}</div>
        <div class="OC2-tablecell OC2-tableStatus">${memberInfo[_key].status}</div>
      </li>`)
    //put all the members currently in crimes below
    if (memberInfo[_key].crimeInfo) {
      $(".OC2-memberTable ul.table-body").append(_outputHTML)
    } else {
      $(".OC2-memberTable ul.table-body").prepend(_outputHTML)
    }

    $(".OC2-memberTableFooter").text(`${availableMembers} / ${totalMembers} members available (${activeMembers} in an OC)`)
  }
  styleTable()
}

function generateInsertHTML() {
  _insertHTML = (`
  <div class="category-wrap OC2-memberViewer m-top10">
    <div class="title-black top-round t-overflow">OC 2.0 Member Overview <span class="hideInfoButton">Show all members</span></div>
    <div class="cont-gray OC2-memberTable"><ul class="table-body">
    </ul></div>
    <div class="OC2-memberTableFooter"></div>
  </div>`)
  $("div#faction-crimes").before(_insertHTML)
  $("span.hideInfoButton").off().on("click", event => {
    toggleMemberView()
  })
  styleTable()
}

function toggleMemberView() {
  if ($(".hideInfoButton").hasClass("text-hide")) {
    $(".hideInfoButton").text("Show all members")
    $(".hideInfoButton").removeClass("text-hide")
  } else {
    $(".hideInfoButton").text("Hide active members")
    $(".hideInfoButton").addClass("text-hide")
  }
  $(".OC2-memberInCrime").toggle()
}


function styleTable() {
  $(".OC2-memberTable ul.table-body").css({
    "display": "flex",
    "flex-direction": "column"
  })
  $(".OC2-memberTable a").css({
    "color": "rgb(116, 192, 252)",
    "text-decoration": "none"
  })
  $(".OC2-memberTable li.table-cell").css({
    "display": "flex",
    "flex-direction": "row"
  })
  $(".OC2-memberTable li.OC2-memberAvailable").css({
    "font-weight": "bold"
  })
  $(".OC2-memberTable li.OC2-memberInCrime").css({
    "color": "rgb(153, 153, 153)",
  })
  $(".OC2-memberTable div").css({
    "font-size": "11px",
    "line-height": "11px",
    "padding": "5px 5px 5px 10px"
  })
  $(".OC2-memberTable div.OC2-tablecell").css({
    "display": "inline"
  })
  $(".OC2-memberTable div.OC2-tableMember").css({
    "width": "200px"
  })
  $(".OC2-memberTable div.OC2-tableAvailability").css({
    "width": "250px",
  })
  $(".OC2-memberTable div.OC2-tableCrimeSlot").css({
    "width": "100px"
  })
  $(".OC2-memberTable div.OC2-tableStatus").css({
    "width": "200px"
  })
  $(".OC2-memberTableFooter").css({
    "border-radius": "0 0 5px 5px",
    "background-color": "rgb(51, 51, 51)",
    "padding": "5px 5px 5px 10px",
    "text-align": "center"
  })
  $(".hideInfoButton").css({
    "position": "absolute",
    "right": "10px",
    "cursor": "pointer"
  })
  //light mode styling
  if ($("body").css("background-color") == "rgb(204, 204, 204)") {
    $(".OC2-memberTable a").css({
      "color": "#006699",
    })
    $(".OC2-memberTableFooter").css({
      "background-color": "rgb(242, 242, 242)"
    })
  }
  if (_isWindowSmall.matches) {
    styleTableSmallScreen()
  }
}

function styleTableSmallScreen() {
  $(".OC2-memberTable div.OC2-tableCrimeSlot").css({
    "display": "none"
  })
  $(".OC2-memberTable div.OC2-tableStatus").css({
    "display": "none"
  })
}

function insertOCNotifier() {
  let _userNotice = (`<a href="https://www.torn.com/factions.php?step=your#/tab=crimes"><span class="OC2-redtext">No active OC.</span></a>`)
  if (userInfo.crimeInfo) {
    _userNotice = (`<span class="OC2-normaltext"><a href="https://www.torn.com/factions.php?step=your&type=5#/tab=crimes&crimeId=${userInfo.crimeInfo.crimeId}">${userInfo.crimeInfo.crimePosition} of ${userInfo.crimeInfo.crimeName} (Lv ${userInfo.crimeInfo.crimeDifficulty})</a></span>`)
  }
  let _insertHTML = (`<div class="OC2-sidebarNotice"><a href="https://www.torn.com/factions.php?step=your#/tab=crimes"><span style="font-weight: bold">OC 2.0:</span></a>
    ${_userNotice}
  </div>`)
  $('div[class^="sidebar_"] div[class^="user-information_"] div[class^="toggle-block_"] div[class^="toggle-content_"] div[class^="content_"]').append(_insertHTML)
  styleOCNotifier()
}

function styleOCNotifier() {
  $(".OC2-sidebarNotice a").css({
    "text-decoration": "none",
    "color": "inherit"
  })
  $(".OC2-sidebarNotice .OC2-redtext").css({
    "text-decoration": "none",
    "color": "rgb(255, 121, 76)"
  })
}

/* if page is the crimes 2.0 page
 *  -> if memberViewer table does NOT exist, get data and fill table
 *  -> otherwise, show the table
 * -> otherwise, hide the table
 */
async function hashChangeFunction() {
  let pageURL = $(location).attr("href");
  if (checkCrimesPage(pageURL)) {
    //insert member overview
    if (!$(".OC2-memberViewer")[0]) {
      generateInsertHTML();
      putMemberInfoIntoTable();
    } else {
      $(".OC2-memberViewer").show()
    }
  } else {
    if ($(".OC2-memberViewer")[0]) {
      $(".OC2-memberViewer").hide();
    }
  }
  $(".OC2-memberInCrime").hide();
}

async function runOnceFunction() {
  //exit if API key doesn't exist
  await getAPIKey()
    .then(function(data) {
    if (APIKey == null || APIKey == "") {
      return;
    }
  })
  //get data
  const [_memberData, _crimesData] = await Promise.all([
    getFactionMembers(),
    getCrimesList()
  ])
  //analyze data
  checkMembersInCrimes(_memberData, _crimesData)
  if (!$(".OC2-sidebarNotice")[0]) {
    insertOCNotifier()
  }
  let pageURL = $(location).attr("href");
  if (checkCrimesPage(pageURL)) {
    //insert member overview
    if (!$(".OC2-memberViewer")[0]) {
      generateInsertHTML();
    } else {
      $(".OC2-memberViewer").show()
    }
  }
  putMemberInfoIntoTable();
  $(".OC2-memberInCrime").hide();
}

async function viewTableWhileTraveling() {
  await getAPIKey()
    .then(function(data) {
    if (APIKey == null || APIKey == "") {
      return;
    }
  })
  let _factionInfo = await $.getJSON(`https://api.torn.com/v2/faction/basic?key=${APIKey}`)
    .then(function(data) {
      return data;
    });
  let pageURL = $(location).attr("href")
  waitForElm('div#react-root').then((elm) => {
    if ($(".OC2-memberTable")[0]) {
      return
    }
    if (pageURL.search("ID="+_factionInfo.basic.id) > 0) {
      var _insertHTML = (`
        <div class="category-wrap OC2-memberViewer m-top10">
          <div class="title-black top-round t-overflow">OC 2.0 Member Overview <span class="hideInfoButton">Show all members</span></div>
          <div class="cont-gray OC2-memberTable"><ul class="table-body">
          </ul></div>
          <div class="OC2-memberTableFooter"></div>
        </div>`)
      $(elm).before(_insertHTML)
      $("span.hideInfoButton").on("click", event => {
        toggleMemberView()
      })
      styleTable()
    }
  })
}

//waitForElm from stackoverflow https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
function waitForElm(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }
        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                observer.disconnect();
                resolve(document.querySelector(selector));
            }
        });
        // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

runOnceFunction()
$(window).on('hashchange', hashChangeFunction);
if (($(body).attr("data-traveling") == "true") || ($(body).attr("data-traveling") == true)) {
  viewTableWhileTraveling()
}

_isWindowSmall.addEventListener("change", function() {
  styleTable()
});