YATA

Displays various informations from YATA's API

  1. // ==UserScript==
  2. // @name YATA
  3. // @namespace yata.yt
  4. // @version 0.22.0
  5. // @description Displays various informations from YATA's API
  6. // @author Kivou [2000607]
  7. // @match https://www.torn.com/factions.php*
  8. // @match https://www.torn.com/preferences.php*
  9. // @match https://www.torn.com/profiles.php*
  10. // @match https://www.torn.com/page.php?sid=UserList*
  11. // @icon https://yata.yt/media/yata-small.png
  12. // @require https://update.greatest.deepsurf.us/scripts/477604/1287854/kiv-lib.js
  13. // @require https://update.greatest.deepsurf.us/scripts/479408/1277647/kib-key.js
  14. // @grant GM.xmlHttpRequest
  15. // @run-at document-end
  16. // @license WTFPL
  17. // ==/UserScript==
  18.  
  19. // Copyright © 2024 Kivou [2000607] <n25c4ejn@duck.com>
  20. // This work is free. You can redistribute it and/or modify it under the
  21. // terms of the Do What The Fuck You Want To Public License, Version 2,
  22. // as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.
  23.  
  24. console.log("YATA script loaded");
  25.  
  26. // ------------- //
  27. // SETUP API KEY //
  28. // ------------- //
  29. storeKey("yata", "YATA")
  30.  
  31. // -------------- //
  32. // OC: NNB + rank //
  33. // -------------- //
  34. const display_nnb = (members, player) => {
  35. const urlParams = new URLSearchParams(player.children[0].children[0].href.split("?")[1]);
  36. const lvl = player.children[1].innerText.trim();
  37. if (members.members && members.members.hasOwnProperty(urlParams.get("XID"))) {
  38. const m = members.members[urlParams.get("XID")];
  39. if (m.nnb_share > 0) {
  40. player.children[1].innerHTML = `<span>#<b>${m.crimes_rank}</b> / <b>${m.nnb}</b> / ${lvl}</span>`;
  41. } else if (m.nnb_share < 0) {
  42. player.children[1].innerHTML = `<span title="Not on YATA">#<b>${m.crimes_rank}</b> / <b>!</b> / ${lvl}</span>`;
  43. } else {
  44. player.children[1].innerHTML = `<span title="Not sharing NNB">#<b>${m.crimes_rank}</b> / <b>?</b> / ${lvl}</span>`;
  45. }
  46. } else {
  47. player.children[1].innerHTML = `<span title="Not found">#<b>?</b> / <b>err</b> / ${lvl}</span>`;
  48. }
  49. };
  50.  
  51. waitFor(document, "div#faction-crimes").then(div => {
  52.  
  53. const key = localStorage.getItem('yata-key');
  54.  
  55. if (!key) { return; }
  56.  
  57. const profile_url = new URLSearchParams(window.location.search);
  58. const target_id = profile_url.get("XID");
  59.  
  60. gmGet(`https://yata.yt/api/v1/faction/members/?key=${key}`, 'nnb').then(members => {
  61.  
  62. // triggered if directly landing on crimes
  63. div.querySelectorAll("ul.details-list, ul.plans-list").forEach(ul => {
  64. ul.querySelectorAll("ul.item").forEach(player => {
  65. display_nnb(members, player);
  66. });
  67. });
  68. div.querySelectorAll("ul.title li.level").forEach(t => {
  69. t.innerHTML = 'Rank / NNB / Level';
  70. });
  71.  
  72. // triggered by clicking on crimes tab
  73. const callback = (mutations, observer) => {
  74. [...mutations].forEach(mutation => {
  75. [...mutation.addedNodes].filter(n => n.className && n.className.includes("faction-crimes-wrap")).forEach(node => {
  76. node.querySelectorAll("ul.details-list, ul.plans-list").forEach(ul => {
  77. const ocs = ul.querySelectorAll("ul.item");
  78. console.log(`[yata] displaying NNB for OC: ${ocs.length}`);
  79. ocs.forEach(player => {
  80. display_nnb(members, player);
  81. });
  82. });
  83. node.querySelectorAll("ul.title li.level").forEach(t => {
  84. t.innerHTML = 'Rank / NNB / Level';
  85. });
  86. });
  87. });
  88. };
  89. const observer = new MutationObserver(callback);
  90. observer.observe(div, { childList: true });
  91.  
  92.  
  93. }).catch(error => {
  94. console.warn(`[yata] ${error.message}`);
  95. if (error.message == "Incorrect key") {
  96. localStorage.removeItem('key');
  97. }
  98. });
  99. });
  100.  
  101.  
  102. // ---------------------- //
  103. // PROFILE //
  104. // ---------------------- //
  105.  
  106. waitFor(document, "a.profile-button-report").then(a => {
  107.  
  108. const div = document.getElementById("profileroot");
  109. const key = localStorage.getItem('yata-key');
  110. const profile_url = new URLSearchParams(a.href.split("#")[0].split("?")[1]);
  111. const target_id = profile_url.get("userID");
  112.  
  113. if(div == null) { return; } // ignore miniprofile
  114. if (!target_id) { return; }
  115. if (!key) { return; }
  116.  
  117. gmGet(`https://yata.yt/api/v1/bs/${target_id}/?key=${key}`, `bs-${target_id}`).then(bs => {
  118.  
  119. let innerHTML = "";
  120. innerHTML += `<hr class="page-head-delimiter m-top10 m-bottom10">`;
  121. innerHTML += `<div>`;
  122. innerHTML += `<b>[YATA]</b> <b>Battle stats</b> ${floatFormat(bs[target_id].total, 3)}`;
  123. innerHTML += ` | <b>Build</b> ${bs[target_id].type} (${bs[target_id].skewness}%)`;
  124. innerHTML += `</div>`;
  125. innerHTML += `<hr class="page-head-delimiter m-top10 m-bottom10">`;
  126. innerHTML += `<div class="clear"></div>`;
  127. const bs_node = document.createElement("div");
  128. bs_node.innerHTML = innerHTML;
  129. div.querySelector("div.profile-wrapper").insertAdjacentElement('afterend', bs_node);
  130. console.log(`[yata] battle stats estimate on profile page: ${target_id}`);
  131.  
  132. }).catch(error => {
  133. console.warn(`[yata] ${error.message}`);
  134. if (error.message == "Incorrect key") {
  135. localStorage.removeItem('key');
  136. }
  137. });
  138.  
  139. });
  140.  
  141. // -------------------------- //
  142. // FACTIONS: Helper functions //
  143. // -------------------------- //
  144. const bse_html = (id, bs) => {
  145.  
  146. if (bs == "loading") {
  147. return `<a style="text-decoration: none; display: inline-block;" href="/loader.php?sid=attack&user2ID=${id}" target="_blank"><span title='Loading' style="color: var(--default-red-color);">...</span></a>`;
  148. }
  149.  
  150. if (!bs.hasOwnProperty("type")) {
  151. return `<a style="text-decoration: none; display: inline-block;" href="/loader.php?sid=attack&user2ID=${id}" target="_blank"><span title='${bs["message"]}' style="color: var(--default-red-color);">err</span></a>`;
  152. }
  153.  
  154. let color = "var(--default-blue-color)";
  155. if (bs.type == "Offensive" && bs.skewness > 20) {
  156. color = "var(--default-red-color)";
  157. } else if (bs.type == "Defensive" && bs.skewness > 20) {
  158. color = "var(--default-green-color)";
  159. }
  160. const title = `Total stats: ${bs.total.toLocaleString("en-GB")} Score: ${bs.score.toLocaleString("en-GB")} Build: ${bs.type} (${bs.skewness}%) Version: ${bs.version}`;
  161. return `<a style="text-decoration: none; display: inline-block;" href="/loader.php?sid=attack&user2ID=${id}" target="_blank"><span title="${title}" style="color: ${color};">${floatFormat(bs.total, 3)}</span></a>`;
  162. };
  163.  
  164. // ---------------------- //
  165. // FACTIONS: Members list //
  166. // ---------------------- //
  167. const members_list_filter = (div) => {
  168. if (div.tagName != "DIV") { return false; }
  169. if (div.classList.contains("faction-info-wrap")) { return true; }
  170. return Boolean(div.querySelector("div.faction-info-wrap"));
  171. };
  172.  
  173. const members_list_display = (members, key) => {
  174. console.log(`[yata] battle stats estimate for members list: ${members.length}`);
  175. [...members].forEach(member => {
  176. const member_id = getPlayerId(member, "honor");
  177.  
  178. gmGet(`https://yata.yt/api/v1/bs/${member_id}/?key=${key}`, `bs-${member_id}`).then(bs => {
  179. const node = document.createElement("span");
  180. node.innerHTML = bse_html(member_id, bs[member_id]);
  181. node.style.width = "4em";
  182. // member.querySelector("div.member-icons").insertAdjacentElement('afterbegin', node);
  183. member.querySelector("div.position").insertAdjacentElement('afterbegin', node);
  184. }).catch((error) => {
  185. const node = document.createElement("span");
  186. node.innerHTML = bse_html(member_id, error);
  187. node.style.width = "4em";
  188. // member.querySelector("div.member-icons").insertAdjacentElement('afterbegin', node);
  189. member.querySelector("div.position").insertAdjacentElement('afterbegin', node);
  190. });
  191. });
  192. };
  193.  
  194. // -------------- //
  195. // FACTIONS: Wars //
  196. // -------------- //
  197. const _is_attack_link = (node) => {
  198. return node.tagName == "A" && node.classList.contains("t-blue") || node.tagName == "SPAN" && node.classList.contains("t-gray-9")
  199. }
  200.  
  201. const wars_list_filter = (node) => {
  202. if(_is_attack_link(node)) { return true; }
  203. if (node.tagName != "DIV") { return false; }
  204. return node.tagName == "DIV" && node.classList.contains("faction-war");
  205. };
  206.  
  207. const wars_list_display = (members, type, key) => {
  208. console.log(`[yata] battle stats estimate for ${type}: ${members.length}`);
  209.  
  210. [...members].forEach(member => {
  211. // STEP 1: get target ID
  212.  
  213. let member_id = undefined;
  214.  
  215. if (type == "wall") {
  216. member_id = getPlayerId(member, "username");
  217. } else {
  218. member_id = getPlayerId(member, "honor");
  219. }
  220.  
  221. // STEP 2: make call
  222. gmGet(`https://yata.yt/api/v1/bs/${member_id}/?key=${key}`, `bs-${member_id}`).then(bs => {
  223. const node = document.createElement("span");
  224. node.innerHTML = bse_html(member_id, bs[member_id]);
  225.  
  226. if (type == "wall") {
  227. const step = new URLSearchParams(window.location.search).get('step');
  228. if (step == "your") {
  229. // own faction wall: replace attack link
  230. node.style.paddingRight = "0.5em";
  231. member.children[4].innerHTML = node.outerHTML;
  232. } else {
  233. // waching another faction wall: replace ID
  234. member.children[1].innerHTML = node.outerHTML;
  235. }
  236. } else if (type == "rank-right") {
  237. // prepend to level
  238. node.style.paddingRight = "1em";
  239. member.children[1].insertAdjacentElement('afterbegin', node);
  240. } else if (type == "rank-left") {
  241. // replace link
  242. member.children[4].children[0].style.display = "None"
  243. node.style.paddingRight = "0.5";
  244. member.children[4].insertAdjacentElement('afterbegin', node);
  245. $ } else {
  246. // replace attack link
  247. node.style.paddingRight = "0.5em";
  248. member.children[5].innerHTML = node.outerHTML;
  249. }
  250. }).catch((error) => {
  251. const node = document.createElement("span");
  252. node.innerHTML = bse_html(member_id, error);
  253.  
  254. if (type == "wall") {
  255. // replace attack link
  256. node.style.paddingRight = "0.5em";
  257. member.children[4].innerHTML = node.outerHTML;
  258. } else if (type == "rank-right") {
  259. // prepend to level
  260. node.style.paddingRight = "1em";
  261. member.children[1].insertAdjacentElement('afterbegin', node);
  262. } else if (type == "rank-left") {
  263. // replace link
  264. node.style.paddingRight = "0.5";
  265. member.children[4].innerHTML = node.outerHTML;
  266. } else {
  267. // replace attack link
  268. node.style.paddingRight = "0.5em";
  269. member.children[5].innerHTML = node.outerHTML;
  270. }
  271. });
  272. });
  273. };
  274.  
  275. const chain_list_filter = (node) => {
  276. if (node.tagName != "UL") { return false; }
  277. return node.tagName == "UL" && node.classList.contains("chain-attacks-list");
  278. };
  279.  
  280. const chain_list_display = (attacks, key) => {
  281. console.log(`[yata] battle stats estimate for chain: ${attacks.length}`);
  282.  
  283. const _f = (e) => { return Boolean(e.querySelector("div.right-player div[class^=userInfoBox]")); };
  284. [...attacks].filter(_f).forEach(attack => {
  285.  
  286. const target = attack.querySelector("div.right-player");
  287. const target_id = getPlayerId(target, 'icon')
  288. const respect = attack.querySelector("div.respect");
  289.  
  290. gmGet(`https://yata.yt/api/v1/bs/${target_id}/?key=${key}`, `bs-${target_id}`).then(bs => {
  291. const node = document.createElement("span");
  292. node.innerHTML = bse_html(target_id, bs[target_id]);
  293. node.style.float = 'right';
  294. respect.insertAdjacentElement('beforeend', node);
  295. }).catch((error) => {
  296. const node = document.createElement("span");
  297. node.innerHTML = bse_html(target_id, error);
  298. node.style.float = 'right';
  299. respect.insertAdjacentElement('beforeend', node);
  300. });
  301.  
  302. });
  303. };
  304.  
  305. // ------------------- //
  306. // FACTIONS: observers //
  307. // ------------------- //
  308. const callback_factions = (key) => {
  309. return (mutations, observer) => {
  310.  
  311. const tab = window.location.hash.replace("#/tab=", "");
  312. if (["territory", "rank", "upgrades", "armoury", "controls"].includes(tab)) { return; }
  313.  
  314. [...mutations].forEach(mutation => {
  315.  
  316. [...mutation.addedNodes].forEach(node => {
  317.  
  318. // members list
  319. if (members_list_filter(node)) {
  320. const members = node.querySelectorAll("li.table-row");
  321. if (members.length) { members_list_display(members, key); }
  322. }
  323.  
  324. // chains
  325. if (chain_list_filter(node)) {
  326. chain_list_display(node.childNodes, key);
  327.  
  328. // observe new attacks
  329. const callback_chain = (key) => {
  330. return (mutations, observer) => {
  331. [...mutations].forEach(mutation => {
  332. const attacks = [...mutation.addedNodes];
  333. chain_list_display(attacks, key);
  334. });
  335. };
  336. };
  337. const walls_observer = new MutationObserver(callback_chain(key));
  338. walls_observer.observe(node, { childList: true });
  339. }
  340.  
  341. // wars
  342. if (wars_list_filter(node)) {
  343. // force display none for links
  344. if(_is_attack_link(node)) { node.style.display = "None"; return true; }
  345.  
  346. // 2 lists for RW, 1 for walls and 1 for raids
  347. [...node.querySelectorAll("div.members-cont > ul.members-list")].forEach(faction => {
  348. const members = faction.querySelectorAll("li.your, li.enemy");
  349.  
  350. if (tab.includes("rank")) { // RW
  351. if (Boolean(members[0].querySelector("div.attack").offsetParent)) {
  352. wars_list_display(members, "rank-left", key);
  353. } else {
  354. wars_list_display(members, "rank-right", key);
  355. }
  356. } else if (tab.includes("raid")) { // Raids
  357. wars_list_display(members, "raid", key);
  358. } else { // walls
  359. wars_list_display(members, "wall", key);
  360.  
  361. // observe wall jumps
  362. const callback_walls = (key) => {
  363. return (mutations, observer) => {
  364. const _f = (e) => { return e.className.includes("your") || e.className.includes("enemy"); };
  365. [...mutations].forEach(mutation => {
  366. const members = [...mutation.addedNodes].filter(_f);
  367. wars_list_display(members, "wall", key);
  368. });
  369. };
  370. };
  371. const walls_observer = new MutationObserver(callback_walls(key));
  372. walls_observer.observe(faction, { childList: true });
  373. }
  374. });
  375. }
  376.  
  377. });
  378. });
  379. };
  380. };
  381.  
  382. waitFor(document, "div#factions").then(factions => {
  383.  
  384. const key = localStorage.getItem('yata-key');
  385. if (!key) { return; }
  386.  
  387. const obs = new MutationObserver(callback_factions(key));
  388. obs.observe(factions, { childList: true, subtree: true });
  389.  
  390. });
  391.  
  392.  
  393. // ------ //
  394. // SEARCH //
  395. // ------ //
  396.  
  397. const search_list_display = (players, key) => {
  398. console.log(`[yata] battle stats estimate for search list: ${players.length}`);
  399.  
  400. const _f = (e) => {
  401. return e.tagName == "LI" &&
  402. !e.classList.contains("last") &&
  403. !e.querySelector("[id^=icon15]") &&
  404. !e.querySelector("[id^=icon70]") &&
  405. !e.querySelector("[id^=icon77]") &&
  406. !e.querySelector("[id^=icon71]");
  407. };
  408. [...players].filter(_f).forEach(player => {
  409.  
  410. const target_id = getPlayerId(player, "username")
  411. const level = player.querySelector("span.level");
  412. const node = document.createElement("span");
  413. node.innerHTML = bse_html(target_id, "loading");
  414. node.style.float = 'left';
  415. level.insertAdjacentElement('afterbegin', node);
  416.  
  417. gmGet(`https://yata.yt/api/v1/bs/${target_id}/?key=${key}`, `bs-${target_id}`).then(bs => {
  418. node.innerHTML = bse_html(target_id, bs[target_id]);
  419. }).catch((error) => {
  420. node.innerHTML = bse_html(target_id, error);
  421. });
  422.  
  423. });
  424. };
  425.  
  426. const callback_search = (key) => {
  427. return (mutations, observer) =>{
  428. [...mutations].filter(m => m.addedNodes.length).forEach(players => {
  429. search_list_display(players.addedNodes, key);
  430. })
  431. }
  432. }
  433.  
  434. waitFor(document, "div.userlist-wrapper").then(list => {
  435.  
  436. const key = localStorage.getItem('yata-key');
  437. if (!key) { return; }
  438.  
  439. const ul = list.querySelector("ul.user-info-list-wrap");
  440. if (!ul) { return; }
  441.  
  442.  
  443. const obs = new MutationObserver(callback_search(key));
  444. obs.observe(ul, { childList: true });
  445.  
  446. })