Torn War Stuff Enhanced

Show travel status and hospital time and sort by hospital time on war page. Fork of https://greasyfork.org/en/scripts/448681-torn-war-stuff

// ==UserScript==
// @name         Torn War Stuff Enhanced
// @namespace    namespace
// @version      1.1
// @description  Show travel status and hospital time and sort by hospital time on war page. Fork of https://greasyfork.org/en/scripts/448681-torn-war-stuff
// @author       xentac
// @license      MIT
// @match        *.torn.com/factions.php*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// ==/UserScript==

(async function () {
  ("use strict");

  let apiKey =
    localStorage.getItem("xentac-torn_war_stuff_enhanced-apikey") ??
    "###PDA-APIKEY###";
  const sort_enemies = true;
  const CONTENT = "data-twse-content";
  const TRAVELING = "data-twse-traveling";
  const HIGHLIGHT = "data-twse-highlight";

  try {
    GM_registerMenuCommand("Set Api Key", function () {
      checkApiKey(false);
    });
  } catch (error) {
    // This is fine, but we need to handle torn pda too
  }

  function checkApiKey(checkExisting = true) {
    if (
      !checkExisting ||
      apiKey === null ||
      apiKey.indexOf("PDA-APIKEY") > -1 ||
      apiKey.length != 16
    ) {
      let userInput = prompt(
        "Please enter a PUBLIC Api Key, it will be used to get basic faction information:",
        apiKey ?? "",
      );
      if (userInput !== null && userInput.length == 16) {
        apiKey = userInput;
        localStorage.setItem(
          "xentac-torn_war_stuff_enhanced-apikey",
          userInput,
        );
      } else {
        console.error(
          "[TornWarStuffEnhanced] User cancelled the Api Key input.",
        );
      }
    }
  }

  GM_addStyle(`
.members-list li:has(div.status[data-twse-highlight="true"]) {
  background-color: #afa5 !important;
}
`);

  GM_addStyle(`
.members-list div.status[data-twse-traveling="true"]::after {
  color: #F287FF !important;
}
`);

  GM_addStyle(`
.members-list div.status {
  position: relative !important;
  color: transparent !important;
}
.members-list div.status::after {
  content: attr(data-twse-content);
  position: absolute;
  top: 0;
  left: 0;
  width: calc(100% - 10px);
  height: 100%;
  background: inherit;
  display: flex;
  right: 10px;
  justify-content: flex-end;
  align-items: center;
}
.members-list .ok.status::after {
    color: var(--user-status-green-color);
}


.members-list .not-ok.status::after {
    color: var(--user-status-red-color);
}

.members-list .abroad.status::after, .members-list .traveling.status::after {
    color: var(--user-status-blue-color);
}
`);

  let running = true;

  function get_faction_ids() {
    const nodes = document.querySelectorAll("UL.members-list");
    if (nodes.length != 2) {
      return [];
    }
    const enemy_faction_id = nodes[0]
      .querySelector(`A[href^='/factions.php']`)
      .href.split("ID=")[1];
    const your_faction_id = nodes[1]
      .querySelector(`A[href^='/factions.php']`)
      .href.split("ID=")[1];
    return [enemy_faction_id, your_faction_id];
  }

  function get_member_lists() {
    return document.querySelectorAll("ul.members-list");
  }

  setInterval(() => {
    update_statuses();

    // Backup in case the observer doesn't work
    if (watcher == null) {
      extract_all_member_lis();
      create_watcher();
    }
  }, 5000);

  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      for (const node of mutation.addedNodes) {
        if (node.classList && node.classList.contains("faction-war")) {
          console.log("Caught a mutation");

          update_statuses();
          clear_watchers();
          extract_all_member_lis();
          create_watcher();
        }
      }
    }
  });

  function pad_with_zeros(n) {
    if (n < 10) {
      return "0" + n;
    }
    return n;
  }

  const wrapper = document.body; //.querySelector('#mainContainer')
  observer.observe(wrapper, { subtree: true, childList: true });

  const member_status = new Map();
  const member_lis = new Map();
  let watcher = null;

  let last_request = null;
  const MIN_TIME_SINCE_LAST_REQUEST = 9000;

  async function update_statuses() {
    if (!running) {
      return;
    }
    if (
      last_request &&
      new Date() - last_request < MIN_TIME_SINCE_LAST_REQUEST
    ) {
      return;
    }
    last_request = new Date();
    const faction_ids = get_faction_ids();
    for (let i = 0; i < faction_ids.length; i++) {
      if (!update_status(faction_ids[i])) {
        return;
      }
    }
  }

  async function update_status(faction_id) {
    let error = false;
    const status = await fetch(
      `https://api.torn.com/faction/${faction_id}?selections=basic&key=${apiKey}`,
    )
      .then((r) => r.json())
      .catch((m) => {
        console.error("[TornWarStuffEnhanced] ", m);
        error = true;
      });
    if (error) {
      return true;
    }
    if (status.error) {
      console.log(
        "[TornWarStuffEnhanced] Received error from torn API ",
        status.error,
      );
      if (
        [0, 1, 2, 3, 4, 6, 7, 10, 12, 13, 14, 16, 18, 21].includes(status.error)
      ) {
        console.log(
          "[TornWarStuffEnhanced] Received a non-recoverable error. Giving up.",
        );
        running = false;
        return false;
      }
      if ([5, 8, 9].includes(status.error.code)) {
        // 5: Too many requests error code
        // 8: IP block
        // 9: API disabled
        // Try again in 30 + MIN_TIME_SINCE_LAST_REQUEST seconds
        console.log("[TornWarStuffEnhanced] Retrying in 40 seconds.");
        last_request = new Date() + 30000;
      }
      return false;
    }
    if (!status.members) {
      return false;
    }
    for (const [k, v] of Object.entries(status.members)) {
      v.status.description = v.status.description
        .replace("South Africa", "SA")
        .replace("Cayman Islands", "CI")
        .replace("United Kingdom", "UK")
        .replace("Argentina", "Arg")
        .replace("Switzerland", "Switz");
      member_status.set(k, v);
    }
  }

  function extract_all_member_lis() {
    get_member_lists().forEach((ul) => {
      extract_member_lis(ul);
    });
  }

  function extract_member_lis(ul) {
    const lis = ul.querySelectorAll("LI");
    lis.forEach((li) => {
      const id = li
        .querySelector(`A[href^='/profiles.php']`)
        .href.split("ID=")[1];
      member_lis.set(id, li);
    });
  }

  function create_watcher() {
    watcher = setInterval(() => {
      if (!running) {
        return;
      }
      member_lis.forEach((li, id) => {
        const state = member_status.get(id);
        const status_DIV = li.querySelector("DIV.status");
        if (!state) {
          // Make sure the user sees something before we've downloaded state
          status_DIV.setAttribute(CONTENT, status_DIV.innerText);
          return;
        }
        const status = state.status;

        li.setAttribute("data-until", status.until);
        switch (status.state) {
          case "Abroad":
          case "Traveling":
            if (
              !(
                status_DIV.classList.contains("traveling") ||
                status_DIV.classList.contains("abroad")
              )
            ) {
              status_DIV.setAttribute(CONTENT, status_DIV.innerText);
              break;
            }
            if (status.description.includes("Traveling to ")) {
              li.setAttribute("data-sortA", "4");
              const content =
                "► " + status.description.split("Traveling to ")[1];
              status_DIV.setAttribute(CONTENT, content);
            } else if (status.description.includes("In ")) {
              li.setAttribute("data-sortA", "3");
              const content = status.description.split("In ")[1];
              status_DIV.setAttribute(CONTENT, content);
            } else if (status.description.includes("Returning")) {
              li.setAttribute("data-sortA", "2");
              const content =
                "◄ " + status.description.split("Returning to Torn from ")[1];
              status_DIV.setAttribute(CONTENT, content);
            } else if (status.description.includes("Traveling")) {
              li.setAttribute("data-sortA", "5");
              const content = "Traveling";
              status_DIV.setAttribute(CONTENT, content);
            }
            break;
          case "Hospital":
          case "Jail":
            if (
              !(
                status_DIV.classList.contains("hospital") ||
                status_DIV.classList.contains("jail")
              )
            ) {
              status_DIV.setAttribute(CONTENT, status_DIV.innerText);
              status_DIV.setAttribute(TRAVELING, "false");
              status_DIV.setAttribute(HIGHLIGHT, "false");
              break;
            }
            li.setAttribute("data-sortA", "1");
            if (status.description.includes("In a")) {
              status_DIV.setAttribute(TRAVELING, "true");
            } else {
              status_DIV.setAttribute(TRAVELING, "false");
            }

            const hosp_time_remaining = Math.round(
              status.until - new Date().getTime() / 1000,
            );
            if (hosp_time_remaining <= 0) {
              status_DIV.setAttribute(HIGHLIGHT, "false");
              return;
            }
            const s = Math.floor(hosp_time_remaining % 60);
            const m = Math.floor((hosp_time_remaining / 60) % 60);
            const h = Math.floor(hosp_time_remaining / 60 / 60);
            const time_string = `${pad_with_zeros(h)}:${pad_with_zeros(m)}:${pad_with_zeros(s)}`;

            if (status_DIV.getAttribute(CONTENT) != time_string) {
              status_DIV.setAttribute(CONTENT, time_string);
            }

            if (hosp_time_remaining < 300) {
              status_DIV.setAttribute(HIGHLIGHT, "true");
            } else {
              status_DIV.setAttribute(HIGHLIGHT, "false");
            }
            break;

          default:
            status_DIV.setAttribute(CONTENT, status_DIV.innerText);
            li.setAttribute("data-sortA", "0");
            status_DIV.setAttribute(TRAVELING, "false");
            status_DIV.setAttribute(HIGHLIGHT, "false");
            break;
        }
      });
      if (sort_enemies) {
        const nodes = get_member_lists();
        for (let i = 0; i < nodes.length; i++) {
          let lis = nodes[i].querySelectorAll("LI");
          let sorted_lis = Array.from(lis).sort((a, b) => {
            return (
              a.getAttribute("data-sortA") - b.getAttribute("data-sortA") ||
              a.getAttribute("data-until") - b.getAttribute("data-until")
            );
          });
          let sorted = true;
          for (let j = 0; j < sorted_lis.length; j++) {
            if (nodes[i].children[j] !== sorted_lis[j]) {
              sorted = false;
              break;
            }
          }
          if (!sorted) {
            sorted_lis.forEach((li) => {
              nodes[i].appendChild(li);
            });
          }
        }
      }
    }, 250);
  }

  function clear_watchers() {
    clearInterval(watcher);
    watcher = null;
  }
})();