ASF STM

ASF bot list trade matcher

当前为 2022-12-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ASF STM
  3. // @language English
  4. // @namespace https://greasyfork.org/users/2205
  5. // @description ASF bot list trade matcher
  6. // @license Apache-2.0
  7. // @author Ryzhehvost
  8. // @include http*://steamcommunity.com/id/*/badges
  9. // @include http*://steamcommunity.com/id/*/badges/
  10. // @include http*://steamcommunity.com/profiles/*/badges
  11. // @include http*://steamcommunity.com/profiles/*/badges/
  12. // @version 2.8
  13. // @connect asf.justarchi.net
  14. // @grant GM.xmlHttpRequest
  15. // @grant GM_xmlhttpRequest
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. "use strict";
  20. //configuration
  21. const weblimiter = 300;
  22. const errorLimiter = 30000;
  23. const debug = false;
  24. const maxErrors = 3;
  25. const botCacheTime = 5 * 60000;
  26. const filterBackgroundColor = 'rgba(23, 26, 33, 0.8)';//'rgba(103, 193, 245, 0.8)';
  27.  
  28. //do not change
  29. let myProfileLink = "";
  30. let errors = 0;
  31. let bots = null;
  32. let myBadges = [];
  33. let botBadges = [];
  34. let maxPages;
  35. let stop = false;
  36. let classIdsDB = JSON.parse(localStorage.getItem("Ryzhehvost.ASF.STM"));
  37. if (classIdsDB === null) {
  38. classIdsDB = new Object();
  39. }
  40.  
  41. function debugTime(name) {
  42. if (debug) {
  43. console.time(name);
  44. }
  45. }
  46.  
  47. function debugTimeEnd(name) {
  48. if (debug) {
  49. console.timeEnd(name);
  50. }
  51. }
  52.  
  53. function debugPrint(msg) {
  54. if (debug) {
  55. console.log(new Date().toLocaleTimeString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit",second: "2-digit", fractionalSecondDigits: 3}) + " : " + msg);
  56. }
  57. }
  58.  
  59. function deepClone(object) {
  60. return JSON.parse(JSON.stringify(object));
  61. }
  62.  
  63. function getPartner(str) {
  64. if (typeof(BigInt)!=="undefined") {
  65. return (BigInt(str) % BigInt(4294967296)).toString(); // eslint-disable-line
  66. } else {
  67. let result = 0;
  68. for (let i = 0; i < str.length; i++) {
  69. result = (result * 10 + Number(str[i])) % 4294967296;
  70. }
  71. return result;
  72. }
  73. }
  74.  
  75. function enableButton() {
  76. let buttonDiv = document.getElementById("asf_stm_button_div");
  77. buttonDiv.setAttribute("class", "profile_small_header_additional");
  78. buttonDiv.setAttribute("title", "Scan ASF STM");
  79. let button = document.getElementById("asf_stm_button");
  80. button.addEventListener("click", buttonPressedEvent, false);
  81. }
  82.  
  83. function disableButton() {
  84. let buttonDiv = document.getElementById("asf_stm_button_div");
  85. buttonDiv.setAttribute("class", "profile_small_header_additional btn_disabled");
  86. buttonDiv.setAttribute("title", "Scan is in process");
  87. let button = document.getElementById("asf_stm_button");
  88. button.removeEventListener("click", buttonPressedEvent, false);
  89. }
  90.  
  91. function updateMessage(text) {
  92. let message = document.getElementById("asf_stm_message");
  93. message.textContent = text;
  94. }
  95.  
  96. function hideMessage() {
  97. let messageBox = document.getElementById("asf_stm_messagebox");
  98. messageBox.setAttribute("style", "display: none;");
  99. }
  100.  
  101. function hideThrobber() {
  102. let throbber = document.getElementById("throbber");
  103. throbber.setAttribute("style", "display: none;");
  104. }
  105.  
  106. function updateProgress(index) {
  107. let bar = document.getElementById("asf_stm_progress");
  108. let progress = 100 * ((index + 1) / bots.Result.length);
  109. bar.setAttribute("style", "width: " + progress + "%;");
  110. }
  111.  
  112. function getClassIDs(index) {
  113. updateMessage("Updating cards database for badge " + (index + 1) + " of " + myBadges.length);
  114. debugPrint("getClassIDs for " + myBadges[index].appId);
  115. for (let i = 0; i< myBadges[index].maxCards; i++) {
  116. if (classIdsDB.hasOwnProperty(myBadges[index].appId) && classIdsDB[myBadges[index].appId].hasOwnProperty(myBadges[index].cards[i].item)) {
  117. if (i == myBadges[index].maxCards-1) { //it's last card, so it means we have them all
  118. index++;
  119. if (index < myBadges.length) {
  120. getClassIDs(index);
  121. } else {
  122. debugPrint(JSON.stringify(classIdsDB));
  123. localStorage.setItem("Ryzhehvost.ASF.STM",JSON.stringify(classIdsDB));
  124. setTimeout(function() {
  125. GetCards(0, 0);
  126. }, weblimiter);
  127. }
  128. return;
  129. }
  130. } else {
  131. break; //missing something, update needed
  132. }
  133. }
  134. let searchUrl="https://steamcommunity.com/market/search/render/?start=0&count=30&search_descriptions=0&appid=753&category_753_Game[]=tag_app_"+myBadges[index].appId+"&category_753_cardborder[]=tag_cardborder_0&norender=1"
  135. debugPrint(searchUrl);
  136. let xhr = new XMLHttpRequest();
  137. xhr.open("GET", searchUrl, true);
  138. xhr.responseType = "json";
  139. xhr.onload = function() { // eslint-disable-line
  140. if (stop) {
  141. updateMessage("Interrupted by user");
  142. hideThrobber();
  143. enableButton();
  144. let stopButton = document.getElementById("asf_stm_stop");
  145. stopButton.remove();
  146. return;
  147. }
  148. let status = xhr.status;
  149. if (status === 200) {
  150. let searchResponse = xhr.response;
  151. debugPrint(JSON.stringify(searchResponse));
  152. if (searchResponse.success == true && searchResponse.total_count >= myBadges[index].maxCards) {
  153. let results = searchResponse.results;
  154. for (let cardnumber=0; cardnumber < myBadges[index].maxCards; cardnumber++ ) {
  155. debugPrint("looking for card");
  156. debugPrint(myBadges[index].cards[cardnumber].item);
  157. for (let i = 0; i < results.length; i++) {
  158. debugPrint(results[i].name);
  159. if (results[i].name.trim().startsWith(myBadges[index].cards[cardnumber].item) ||
  160. (myBadges[index].cards[cardnumber].iconUrl.includes(results[i].asset_description.icon_url))) {
  161. debugPrint("found!");
  162. let classid = results[i].asset_description.classid;
  163. if (classIdsDB[myBadges[index].appId] === undefined) {
  164. classIdsDB[myBadges[index].appId] = new Object();
  165. }
  166. let cardsClasses = classIdsDB[myBadges[index].appId];
  167. cardsClasses[myBadges[index].cards[cardnumber].item] = classid;
  168. break;
  169. }
  170. }
  171. if (!(classIdsDB.hasOwnProperty(myBadges[index].appId) && classIdsDB[myBadges[index].appId].hasOwnProperty(myBadges[index].cards[cardnumber].item))) {
  172. //still not found...
  173. updateMessage("Error getting classid for card \"" + myBadges[index].cards[cardnumber].item + "\" from "+ myBadges[index].appId + ", please report this!");
  174. hideThrobber();
  175. enableButton();
  176. let stopButton = document.getElementById("asf_stm_stop");
  177. stopButton.remove();
  178. return;
  179. }
  180. }
  181. } else {
  182. updateMessage("Error getting card data for "+ myBadges[index].appId + ", please report this!");
  183. hideThrobber();
  184. enableButton();
  185. let stopButton = document.getElementById("asf_stm_stop");
  186. stopButton.remove();
  187. return;
  188. }
  189.  
  190. errors = 0;
  191. index++;
  192. if (index < myBadges.length) {
  193. setTimeout((function(index) {
  194. return function() {
  195. getClassIDs(index);
  196. };
  197. })(index), weblimiter+errorLimiter*errors);
  198. } else {
  199. debugPrint(JSON.stringify(classIdsDB));
  200. localStorage.setItem("Ryzhehvost.ASF.STM",JSON.stringify(classIdsDB));
  201. setTimeout(function() {
  202. GetCards(0, 0);
  203. }, weblimiter);
  204. }
  205. return;
  206. } else {
  207. errors++;
  208. }
  209. if ((status < 400 || status >= 500) && (errors <= maxErrors)) {
  210. setTimeout((function(index) {
  211. return function() {
  212. getClassIDs(index);
  213. };
  214. })(index), weblimiter+errorLimiter*errors);
  215. } else {
  216. if (status != 200) {
  217. updateMessage("Error getting classid, ERROR " + status);
  218. } else {
  219. updateMessage("Error getting classid, malformed json");
  220. }
  221. hideThrobber();
  222. enableButton();
  223. let stopButton = document.getElementById("asf_stm_stop");
  224. stopButton.remove();
  225. return;
  226. }
  227.  
  228. };
  229. xhr.onerror = function() { // eslint-disable-line
  230. if (stop) {
  231. updateMessage("Interrupted by user");
  232. hideThrobber();
  233. enableButton();
  234. let stopButton = document.getElementById("asf_stm_stop");
  235. stopButton.remove();
  236. return;
  237. }
  238. errors++;
  239. if (errors <= maxErrors) {
  240. setTimeout((function(index) {
  241. return function() {
  242. getClassIDs(index);
  243. };
  244. })(index), weblimiter+errorLimiter*errors);
  245. return;
  246. } else {
  247. debugPrint("error");
  248. updateMessage("Error getting classid");
  249. hideThrobber();
  250. enableButton();
  251. let stopButton = document.getElementById("asf_stm_stop");
  252. stopButton.remove();
  253. return;
  254. }
  255. };
  256. xhr.send();
  257. }
  258.  
  259. function populateCards(item) {
  260. let classList = "";
  261. let htmlCards = "";
  262. for (let j = 0; j < item.cards.length; j++) {
  263. let itemIcon = item.cards[j].iconUrl+"/98x115";
  264. let itemName = item.cards[j].item.substring(item.cards[j].item.indexOf("-") + 1);
  265. for (let k = 0; k < item.cards[j].count; k++) {
  266. if (classList != "") {
  267. classList += ";";
  268. }
  269. classList += classIdsDB[item.appId][item.cards[j].item];
  270. let cardTemplate = `
  271. <div class="showcase_slot">
  272. <img class="image-container" src="${itemIcon}/98x115">
  273. <div class="commentthread_subscribe_hint" style="width: 98px;">${itemName}</div>
  274. </div>
  275. `;
  276. htmlCards += cardTemplate;
  277. }
  278. }
  279. return {
  280. "htmlCards": htmlCards,
  281. "classList": classList
  282. };
  283. }
  284.  
  285. function getClasses(item) {
  286. let classes = "";
  287. for (let j = 0; j < item.cards.length; j++) {
  288. for (let k = 0; k < item.cards[j].count; k++) {
  289. if (classes != "") {
  290. classes += ";";
  291. }
  292. classes += classIdsDB[item.appId][item.cards[j].item];
  293. }
  294. }
  295. return classes;
  296. }
  297.  
  298. function updateTrade(row) {
  299. let index = row.id.split("_")[1];
  300. let tradeLink = row.getElementsByClassName("full_trade_url")[0];
  301. let splitUrl = tradeLink.href.split("&");
  302. let them = "";
  303. let you = "";
  304. //let filterWidget = document.getElementById("asf_stm_filters_body");
  305. for (let i = 0; i < bots.Result[index].itemsToSend.length; i++) {
  306. let appId = bots.Result[index].itemsToSend[i].appId;
  307. let checkBox = document.getElementById("astm_" + appId);
  308. if (checkBox.checked) {
  309. if (you != "") {
  310. you += ";";
  311. }
  312. you = you + getClasses(bots.Result[index].itemsToSend[i]);
  313. if (them != "") {
  314. them += ";";
  315. }
  316. them = them + getClasses(bots.Result[index].itemsToReceive[i]);
  317. }
  318. }
  319. splitUrl[3] = "them=" + them;
  320. splitUrl[4] = "you=" + you;
  321. tradeLink.href = splitUrl.join("&");
  322. }
  323.  
  324. function checkRow(row) {
  325. debugPrint("checkRow");
  326. let matches = row.getElementsByClassName("badge_row");
  327. let visible = false;
  328. for (let i = 0; i < matches.length; i++) {
  329. if (matches[i].parentElement.style.display != "none") {
  330. visible = true;
  331. break;
  332. }
  333. }
  334. if (visible) {
  335. row.style.display = "block";
  336. updateTrade(row);
  337. } else {
  338. row.style.display = "none";
  339. }
  340. }
  341.  
  342. function addMatchRow(index, botname) {
  343. debugPrint("addMatchRow " + index);
  344. let itemsToSend = bots.Result[index].itemsToSend;
  345. let itemsToReceive = bots.Result[index].itemsToReceive;
  346. let tradeUrl = "https://steamcommunity.com/tradeoffer/new/?partner=" + getPartner(bots.Result[index].SteamID) + "&token=" + bots.Result[index].TradeToken + "&source=stm";
  347. let globalYou = "";
  348. let globalThem = "";
  349. let matches = "";
  350. let any = "";
  351. if (bots.Result[index].MatchEverything == 1) {
  352. any = `&nbsp;<sup><span class="avatar_block_status_in-game" style="font-size: 8px; cursor:help" title="This bots trades for any cards within same set">&nbsp;ANY&nbsp;</span></sup>`;
  353. }
  354. for (let i = 0; i < itemsToSend.length; i++) {
  355. let appId = itemsToSend[i].appId;
  356. let itemToReceive = itemsToReceive.find(a => a.appId == appId);
  357. let gameName = itemsToSend[i].title;
  358. let display = "inline-block";
  359.  
  360. //remove placeholder
  361. let filterWidget = document.getElementById("asf_stm_filters_body");
  362. let placeholder = document.getElementById("asf_stm_placeholder");
  363. if (placeholder != null) {
  364. placeholder.remove();
  365. }
  366. //add filter
  367. let checkBox = document.getElementById("astm_" + appId);
  368. if (checkBox == null) {
  369. let newFilter = `<span style="margin-right: 15px; white-space: nowrap; display: inline-block;"><input type="checkbox" id="astm_${appId}" checked="" /><label for="astm_${appId}">${gameName}</label></span>`;
  370. let spanTemplate = document.createElement("template");
  371. spanTemplate.innerHTML = newFilter.trim();
  372. filterWidget.appendChild(spanTemplate.content.firstChild);
  373. } else {
  374. if (checkBox.checked == false) {
  375. display = "none";
  376. }
  377. }
  378.  
  379. let sendResult = populateCards(itemsToSend[i]);
  380. let receiveResult = populateCards(itemToReceive);
  381.  
  382. let tradeUrlApp = tradeUrl + "&them=" + receiveResult.classList + "&you=" + sendResult.classList;
  383.  
  384. let matchTemplate = `
  385. <div class="asf_stm_appid_${appId}" style="display:${display}">
  386. <div class="badge_row is_link goo_untradable_note showcase_slot">
  387. <div class="notLoggedInText">
  388. <img alt="${gameName}" src="https://steamcdn-a.akamaihd.net/steam/apps/${appId}/capsule_184x69.jpg">
  389. <div>
  390. <div title="View badge progress for this game">
  391. <a target="_blank" rel="noopener noreferrer" href="https://steamcommunity.com/${myProfileLink}/gamecards/${appId}/">${gameName}</a>
  392. </div>
  393. </div>
  394. <div class="btn_darkblue_white_innerfade btn_medium">
  395. <span>
  396. <a href="${tradeUrlApp}" target="_blank" rel="noopener noreferrer">Offer a trade</a>
  397. </span>
  398. </div>
  399. </div>
  400. <div class="showcase_slot">
  401. <div class="showcase_slot profile_header">
  402. <div class="badge_info_unlocked profile_xp_block_mid avatar_block_status_in-game badge_info_title badge_row_overlay" style="height: 15px;">You</div>
  403. ${sendResult.htmlCards}
  404. </div>
  405. <span class="showcase_slot badge_info_title booster_creator_actions">
  406. <h1>&#10145;</h1>
  407. </span>
  408. </div>
  409. <div class="showcase_slot profile_header">
  410. <div class="badge_info_unlocked profile_xp_block_mid avatar_block_status_online badge_info_title badge_row_overlay ellipsis" style="height: 15px;">
  411. ${botname}
  412. </div>
  413. ${receiveResult.htmlCards}
  414. </div>
  415. </div>
  416. </div>
  417. `;
  418. if (checkBox == null || checkBox.checked) {
  419. matches += matchTemplate;
  420. if (globalYou != "") {
  421. globalYou += ";";
  422. }
  423. globalYou += sendResult.classList;
  424. if (globalThem != "") {
  425. globalThem += ";";
  426. }
  427. globalThem += receiveResult.classList;
  428. }
  429. }
  430. let tradeUrlFull = tradeUrl + "&them=" + globalThem + "&you=" + globalYou;
  431. let rowTemplate = `
  432. <div id="asfstmbot_${index}" class="badge_row">
  433. <div class="badge_row_inner">
  434. <div class="badge_title_row guide_showcase_contributors">
  435. <div class="badge_title_stats">
  436. <div class="btn_darkblue_white_innerfade btn_medium">
  437. <span>
  438. <a class="full_trade_url" href="${tradeUrlFull}" target="_blank" rel="noopener noreferrer" >Offer a trade for all</a>
  439. </span>
  440. </div>
  441. </div>
  442. <div style="float: left;" class="">
  443. <div class="user_avatar playerAvatar online">
  444. <a target="_blank" rel="noopener noreferrer" href="https://steamcommunity.com/profiles/${bots.Result[index].SteamID}">
  445. <img src="https://avatars.cloudflare.steamstatic.com/${bots.Result[index].AvatarHash}.jpg" />
  446. </a>
  447. </div>
  448. </div>
  449. <div class="badge_title">
  450. &nbsp;<a target="_blank" rel="noopener noreferrer" href="https://steamcommunity.com/profiles/${bots.Result[index].SteamID}">${botname}</a>${any}
  451. &ensp;<span style="color: #8F98A0;">(${bots.Result[index].TotalItemsCount} items)</span>
  452. </div>
  453. </div>
  454. <div class="badge_title_rule"></div>
  455. ${matches}
  456. </div>
  457. </div>
  458. `;
  459. let template = document.createElement("template");
  460. template.innerHTML = rowTemplate.trim();
  461. let mainContentDiv = document.getElementsByClassName("maincontent")[0];
  462. let newChild = template.content.firstChild;
  463. mainContentDiv.appendChild(newChild);
  464. checkRow(newChild);
  465. }
  466.  
  467. function calcState(badge) { //state 0 - less than max sets; state 1 - we have max sets, even out the rest, state 2 - all even
  468. if (badge.cards[badge.maxCards - 1].count == badge.maxSets) {
  469. if (badge.cards[0].count == badge.lastSet) {
  470. return 2; //nothing to do
  471. } else {
  472. return 1; //max sets are here, but we can distribute cards further
  473. }
  474. } else {
  475. return 0; //less than max sets
  476. }
  477. }
  478.  
  479. function compareCards(index, callback) {
  480. let itemsToSend = [];
  481. let itemsToReceive = [];
  482.  
  483. debugPrint("bot's cards");
  484. debugPrint(JSON.stringify(botBadges));
  485. debugPrint("our cards");
  486. debugPrint(JSON.stringify(myBadges));
  487.  
  488. for (let i = 0; i < botBadges.length; i++) {
  489. let myBadge = deepClone(myBadges[i]);
  490. let theirBadge = deepClone(botBadges[i]);
  491. let myState = calcState(myBadge);
  492. debugPrint("state="+myState);
  493. debugPrint("myapp="+myBadge.appId+" botapp="+theirBadge.appId);
  494. while (myState < 2) {
  495. let foundMatch = false;
  496. for (let j = 0; j < theirBadge.maxCards; j++) { //index of card they give
  497. if (theirBadge.cards[j].count > 0) {
  498. //try to match
  499. let myInd = myBadge.cards.findIndex(a => a.item == theirBadge.cards[j].item); //index of slot where we receive card
  500. if ((myState == 0 && myBadge.cards[myInd].count < myBadge.maxSets) ||
  501. (myState == 1 && myBadge.cards[myInd].count < myBadge.lastSet)) { //we need this ^Kfor the Emperor
  502. debugPrint("we need this: " + theirBadge.cards[j].item + " (" + theirBadge.cards[j].count + ")");
  503. //find a card to match.
  504. for (let k = 0; k < myInd; k++) { //index of card we give
  505. debugPrint("i=" + i + " j=" + j + " k=" + k + " myState=" + myState);
  506. debugPrint("we have this: " + myBadge.cards[k].item + " (" + myBadge.cards[k].count + ")");
  507. if ((myState == 0 && myBadge.cards[k].count > myBadge.maxSets) ||
  508. (myState == 1 && myBadge.cards[k].count > myBadge.lastSet)) { //that's fine for us
  509. debugPrint("it's a good trade for us");
  510. let theirInd = theirBadge.cards.findIndex(a => a.item == myBadge.cards[k].item); //index of slot where they will receive card
  511. if (bots.Result[index].MatchEverything == 0) { //make sure it's neutral+ for them
  512. if (theirBadge.cards[theirInd].count >= theirBadge.cards[j].count) {
  513. debugPrint("Not fair for them");
  514. debugPrint("they have this: " + theirBadge.cards[theirInd].item + " (" + theirBadge.cards[theirInd].count + ")");
  515. continue; //it's not neutral+, check other options
  516. }
  517. }
  518. debugPrint("it's a match!");
  519. let itemToSend = {
  520. "item": myBadge.cards[k].item,
  521. "count": 1,
  522. "class": classIdsDB[myBadge.appId][myBadge.cards[k].item],
  523. "iconUrl": myBadge.cards[k].iconUrl
  524. };
  525. let itemToReceive = {
  526. "item": theirBadge.cards[j].item,
  527. "count": 1,
  528. "class": classIdsDB[theirBadge.appId][theirBadge.cards[j].item],
  529. "iconUrl": theirBadge.cards[j].iconUrl
  530. };
  531. //fill items to send
  532. let sendmatch = itemsToSend.find(item => item.appId == myBadges[i].appId);
  533. if (sendmatch == undefined) {
  534. let newMatch = {
  535. "appId": myBadges[i].appId,
  536. "title": myBadge.title,
  537. "cards": [itemToSend]
  538. };
  539. itemsToSend.push(newMatch);
  540. } else {
  541. let existingCard = sendmatch.cards.find(a => a.item == itemToSend.item);
  542. if (existingCard == undefined) {
  543. sendmatch.cards.push(itemToSend);
  544. } else {
  545. existingCard.count += 1;
  546. }
  547. }
  548. //add this item to their inventory
  549. theirBadge.cards[theirInd].count += 1;
  550. //remove this item from our inventory
  551. myBadge.cards[k].count -= 1;
  552.  
  553. //fill items to receive
  554. let receiveMatch = itemsToReceive.find(item => item.appId == myBadges[i].appId);
  555. if (receiveMatch == undefined) {
  556. let newMatch = {
  557. "appId": myBadges[i].appId,
  558. "title": myBadge.title,
  559. "cards": [itemToReceive]
  560. };
  561. itemsToReceive.push(newMatch);
  562. } else {
  563. let existingCard = sendmatch.cards.find(a => a.item == itemToReceive.item);
  564. if (existingCard == undefined) {
  565. receiveMatch.cards.push(itemToReceive);
  566. } else {
  567. existingCard.count += 1;
  568. }
  569. }
  570. //add this item to our inventory
  571. myBadge.cards[myInd].count += 1;
  572. //remove this item from their inventory
  573. theirBadge.cards[j].count -= 1;
  574. foundMatch = true;
  575. break; //found a match!
  576. }
  577. }
  578. }
  579. }
  580. }
  581. if (!foundMatch) {
  582. break; //found no matches - move to next badge
  583. }
  584. myBadge.cards.sort((a, b) => b.count - a.count);
  585. theirBadge.cards.sort((a, b) => b.count - a.count);
  586. myState = calcState(myBadge);
  587. }
  588. }
  589. debugPrint("items to send");
  590. debugPrint(JSON.stringify(itemsToSend));
  591. debugPrint("items to receive");
  592. debugPrint(JSON.stringify(itemsToReceive));
  593. bots.Result[index].itemsToSend = itemsToSend;
  594. bots.Result[index].itemsToReceive = itemsToReceive;
  595. if (itemsToSend.length > 0) {
  596. //getUsername(index, callback);
  597. addMatchRow(index, bots.Result[index].Nickname);
  598. callback();
  599. } else {
  600. debugPrint("no matches");
  601. callback();
  602. }
  603. }
  604.  
  605. function GetCards(index, userindex) {
  606. debugPrint("GetCards "+index+" : "+userindex);
  607. if (index == 0) {
  608. botBadges.length = 0;
  609. botBadges = deepClone(myBadges);
  610. for (let i = 0; i < botBadges.length; i++) {
  611. botBadges[i].cards.length = 0;
  612. }
  613. }
  614. if (index < botBadges.length) {
  615. let profileLink;
  616. if (userindex == -1) {
  617. profileLink = myProfileLink;
  618. updateMessage("Getting our data for badge " + (index + 1) + " of " + botBadges.length);
  619. } else {
  620. profileLink = "profiles/" + bots.Result[userindex].SteamID;
  621. updateMessage("Fetching bot " + (userindex + 1).toString() + " of " + bots.Result.length.toString()+" (badge " + (index + 1) + " of " + botBadges.length+")");
  622. updateProgress(userindex);
  623. }
  624.  
  625. let url = "https://steamcommunity.com/" + profileLink + "/gamecards/" + botBadges[index].appId + "?l=english";
  626. let xhr = new XMLHttpRequest();
  627. xhr.open("GET", url, true);
  628. xhr.responseType = "document";
  629. xhr.onload = function() { // eslint-disable-line
  630. if (stop) {
  631. updateMessage("Interrupted by user");
  632. hideThrobber();
  633. enableButton();
  634. let stopButton = document.getElementById("asf_stm_stop");
  635. stopButton.remove();
  636. return;
  637. }
  638. let status = xhr.status;
  639. if (status === 200) {
  640. debugPrint("processing badge " + botBadges[index].appId);
  641. let badgeCards = xhr.response.documentElement.querySelectorAll(".badge_card_set_card");
  642. if (badgeCards.length >= 5) {
  643. errors = 0;
  644. botBadges[index].maxCards = badgeCards.length;
  645. for (let i = 0; i < badgeCards.length; i++) {
  646. let quantityElement = badgeCards[i].querySelector(".badge_card_set_text_qty");
  647. let quantity = (quantityElement == null)? "(0)": quantityElement.innerText.trim();
  648. quantity = quantity.slice(1, -1);
  649. let name = "";
  650. badgeCards[i].querySelector(".badge_card_set_title").childNodes.forEach(function (element) {
  651. if (element.nodeType === Node.TEXT_NODE) {
  652. name = name + element.textContent;
  653. }
  654. });
  655. name = name.trim();
  656. let icon = badgeCards[i].querySelector(".gamecard").src.trim();
  657. let newcard = {
  658. "item": name,
  659. "count": Number(quantity),
  660. "iconUrl": icon
  661. };
  662. debugPrint(JSON.stringify(newcard));
  663. botBadges[index].cards.push(newcard);
  664. }
  665.  
  666.  
  667. index++;
  668. setTimeout((function(index, userindex) {
  669. return function() {
  670. GetCards(index, userindex);
  671. };
  672. })(index, userindex), weblimiter);
  673. return;
  674. } else { //if can't find any cards on badge page - retry, that's must be a bug.
  675. debugPrint(xhr.response.documentElement.outerHTML);
  676. errors++;
  677. }
  678. } else {
  679. errors++;
  680. }
  681. if ((status < 400 || status >= 500) && (errors <= maxErrors)) {
  682. setTimeout((function(index,userindex) {
  683. return function() {
  684. GetCards(index,userindex);
  685. };
  686. })(index,userindex), weblimiter+errorLimiter*errors);
  687. } else {
  688. if (status != 200) {
  689. updateMessage("Error getting badge data, ERROR " + status);
  690. } else {
  691. updateMessage("Error getting badge data, malformed HTML");
  692. }
  693. hideThrobber();
  694. enableButton();
  695. let stopButton = document.getElementById("asf_stm_stop");
  696. stopButton.remove();
  697. return;
  698. }
  699.  
  700. };
  701. xhr.onerror = function() { // eslint-disable-line
  702. if (stop) {
  703. updateMessage("Interrupted by user");
  704. hideThrobber();
  705. enableButton();
  706. let stopButton = document.getElementById("asf_stm_stop");
  707. stopButton.remove();
  708. return;
  709. }
  710. errors++;
  711. if (errors <= maxErrors) {
  712. setTimeout((function(index,userindex) {
  713. return function() {
  714. GetCards(index,userindex);
  715. };
  716. })(index,userindex), weblimiter+errorLimiter*errors);
  717. return;
  718. } else {
  719. debugPrint("error");
  720. updateMessage("Error getting badge data");
  721. hideThrobber();
  722. enableButton();
  723. let stopButton = document.getElementById("asf_stm_stop");
  724. stopButton.remove();
  725. return;
  726. }
  727. };
  728. xhr.send();
  729. return; //do this synchronously to avoid rate limit
  730. }
  731. debugPrint("populated");
  732.  
  733. debugTime("Filter and sort");
  734. for (let i = botBadges.length - 1; i >= 0; i--) {
  735. debugPrint("badge "+i+JSON.stringify(botBadges[i]));
  736.  
  737. botBadges[i].cards.sort((a, b) => b.count - a.count);
  738. if (userindex < 0) {
  739. if (botBadges[i].cards[0].count - botBadges[i].cards[botBadges[i].cards.length - 1].count < 2) {
  740. //nothing to match, remove from list.
  741. botBadges.splice(i, 1);
  742. continue;
  743. }
  744. }
  745. let totalCards = 0;
  746. for (let j = 0; j < botBadges[i].maxCards; j++) {
  747. totalCards += botBadges[i].cards[j].count;
  748. }
  749. botBadges[i].maxSets = Math.floor(totalCards / botBadges[i].maxCards);
  750. botBadges[i].lastSet = Math.ceil(totalCards / botBadges[i].maxCards);
  751. debugPrint("totalCards="+totalCards+" maxSets="+botBadges[i].maxSets+" lastSet="+botBadges[i].lastSet);
  752. }
  753. debugTimeEnd("Filter and sort");
  754.  
  755. if (userindex < 0) {
  756. if (botBadges.length == 0) {
  757. hideThrobber();
  758. updateMessage("No cards to match");
  759. enableButton();
  760. let stopButton = document.getElementById("asf_stm_stop");
  761. stopButton.remove();
  762. return;
  763. } else {
  764. myBadges = deepClone(botBadges);
  765. getClassIDs(0);
  766. return;
  767. }
  768. } else {
  769. debugPrint(bots.Result[userindex].SteamID);
  770. compareCards(userindex, function() {
  771. if (userindex < bots.Result.length - 1) {
  772. setTimeout((function(userindex) {
  773. return function() {
  774. GetCards(0, userindex);
  775. };
  776. })(userindex + 1), weblimiter);
  777. } else {
  778. debugPrint("finished");
  779. debugPrint(new Date(Date.now()));
  780. hideThrobber();
  781. hideMessage();
  782. updateProgress(bots.Result.length - 1);
  783. enableButton();
  784. let stopButton = document.getElementById("asf_stm_stop");
  785. stopButton.remove();
  786. }
  787. });
  788. }
  789.  
  790. }
  791.  
  792. function getBadges(page) {
  793. let url = "https://steamcommunity.com/" + myProfileLink + "/badges?p=" + page + "&l=english";
  794. let xhr = new XMLHttpRequest();
  795. xhr.open("GET", url, true);
  796. xhr.responseType = "document";
  797. xhr.onload = function() {
  798. if (stop) {
  799. updateMessage("Interrupted by user");
  800. hideThrobber();
  801. enableButton();
  802. let stopButton = document.getElementById("asf_stm_stop");
  803. stopButton.remove();
  804. return;
  805. }
  806. let status = xhr.status;
  807. if (status === 200) {
  808. errors = 0;
  809. debugPrint("processing page " + page);
  810. updateMessage("Processing badges page " + page);
  811. if (page === 1) {
  812. let pageLinks = xhr.response.documentElement.getElementsByClassName("pagelink");
  813. if (pageLinks.length > 0) {
  814. maxPages = Number(pageLinks[pageLinks.length - 1].textContent.trim());
  815. }
  816. }
  817. let badges = xhr.response.documentElement.getElementsByClassName("badge_row_inner");
  818. for (let i = 0; i < badges.length; i++) {
  819. if (badges[i].getElementsByClassName("owned").length > 0) { //we only need badges where we have at least one card, and no special badges
  820. if (!badges[i].parentElement.querySelector(".badge_row_overlay").href.endsWith("border=1")){ //ignore foil badges completely so far.
  821. let appidNodes = badges[i].getElementsByClassName("card_drop_info_dialog");
  822. if (appidNodes.length > 0) {
  823. let appidText = appidNodes[0].getAttribute("id");
  824. let appidSplitted = appidText.split("_");
  825. if (appidSplitted.length >= 5) {
  826. let appId = Number(appidSplitted[4]);
  827. let maxCards = 0;
  828. if (badges[i].getElementsByClassName("badge_craft_button").length === 0) {
  829. let maxCardsText = badges[i].getElementsByClassName("badge_progress_info")[0].innerText.trim();
  830. let maxCardsSplitted = maxCardsText.split(" ");
  831. maxCards = Number(maxCardsSplitted[2]);
  832. }
  833. let title = badges[i].querySelector(".badge_title").childNodes[0].textContent.trim();
  834. let badgeStub = {
  835. "appId": appId,
  836. "title": title,
  837. "maxCards": maxCards,
  838. "maxSets": 0,
  839. "lastSet": 0,
  840. "cards": []
  841. };
  842. myBadges.push(badgeStub);
  843. }
  844. }
  845. }
  846. }
  847. }
  848. page++;
  849. } else {
  850. errors++;
  851. }
  852. if ((status < 400 || status >= 500) && (errors <= maxErrors)) {
  853. if (page <= maxPages) {
  854. setTimeout((function(page) {
  855. return function() {
  856. getBadges(page);
  857. };
  858. })(page), weblimiter+errorLimiter*errors);
  859. } else {
  860. debugPrint("all badge pages processed");
  861. debugPrint(weblimiter+errorLimiter*errors);
  862. if (myBadges.length === 0) {
  863. hideThrobber();
  864. updateMessage("No cards to match");
  865. enableButton();
  866. let stopButton = document.getElementById("asf_stm_stop");
  867. stopButton.remove();
  868. return;
  869. } else {
  870. setTimeout(function() {
  871. GetCards(0,-1);
  872. }, weblimiter+errorLimiter*errors);
  873. }
  874. }
  875. } else {
  876. if (status != 200) {
  877. updateMessage("Error getting badge page, ERROR " + status);
  878. } else {
  879. updateMessage("Error getting badge page, malformed HTML");
  880. }
  881. hideThrobber();
  882. enableButton();
  883. let stopButton = document.getElementById("asf_stm_stop");
  884. stopButton.remove();
  885. return;
  886. }
  887. };
  888. xhr.onerror = function() {
  889. if (stop) {
  890. updateMessage("Interrupted by user");
  891. hideThrobber();
  892. enableButton();
  893. let stopButton = document.getElementById("asf_stm_stop");
  894. stopButton.remove();
  895. return;
  896. }
  897. errors++;
  898. if (errors <= maxErrors) {
  899. setTimeout((function(page) {
  900. return function() {
  901. getBadges(page);
  902. };
  903. })(page), weblimiter+errorLimiter*errors);
  904. } else {
  905. debugPrint("error getting badge page");
  906. updateMessage("Error getting badge page");
  907. hideThrobber();
  908. enableButton();
  909. let stopButton = document.getElementById("asf_stm_stop");
  910. stopButton.remove();
  911. return;
  912. }
  913. };
  914. xhr.send();
  915. }
  916.  
  917. function filterEventHandler(event) {
  918. let appId = event.target.id.split("_")[1];
  919. let matches = document.getElementsByClassName("asf_stm_appid_" + appId);
  920. for (let i = 0; i < matches.length; i++) {
  921. matches[i].style.display = event.target.checked ? "inline-block" : "none";
  922. checkRow(matches[i].parentElement.parentElement);
  923. }
  924. }
  925.  
  926. function filterSwitchesHandler(event) {
  927. let action = event.target.id.split("_")[3];
  928. let filterWidget = document.getElementById("asf_stm_filters_body");
  929. let checkboxes=filterWidget.getElementsByTagName("input");
  930. for (let i = 0; i < checkboxes.length; i++) {
  931. if (action==="all") {
  932. if (!checkboxes[i].checked) {
  933. checkboxes[i].checked=true;
  934. filterEventHandler({"target":checkboxes[i]});
  935. }
  936. } else if (action==="none") {
  937. if (checkboxes[i].checked) {
  938. checkboxes[i].checked=false;
  939. filterEventHandler({"target":checkboxes[i]});
  940. }
  941. } else if (action==="invert") {
  942. checkboxes[i].checked=!checkboxes[i].checked;
  943. filterEventHandler({"target":checkboxes[i]});
  944. }
  945. }
  946. }
  947.  
  948. function filtersButtonEvent() {
  949. let filterWidget = document.getElementById("asf_stm_filters");
  950. if (filterWidget.style.marginRight == "-50%") {
  951. filterWidget.style.marginRight = "unset";
  952. } else {
  953. filterWidget.style.marginRight = "-50%";
  954. }
  955. }
  956.  
  957. function stopButtonEvent() {
  958. let stopButton = document.getElementById("asf_stm_stop");
  959. stopButton.removeEventListener("click", stopButtonEvent, false);
  960. stopButton.title = "Stopping...";
  961. stopButton.classList.add("btn_disabled");
  962. updateMessage("Stopping...");
  963. stop = true;
  964. }
  965.  
  966. function buttonPressedEvent() {
  967. if (bots === null || bots.Result === undefined || bots.Result.length == 0 || bots.Success != true || bots.cacheTime + botCacheTime < Date.now()) {
  968. debugPrint("Bot cache invalidated");
  969. fetchBots();
  970. return;
  971. }
  972. disableButton();
  973. debugPrint(new Date(Date.now()));
  974. let mainContentDiv = document.getElementsByClassName("maincontent")[0];
  975. mainContentDiv.textContent = "";
  976. mainContentDiv.style.width = "90%";
  977. mainContentDiv.innerHTML = `
  978. <div class="profile_badges_header">
  979. <div id="throbber">
  980. <div class="LoadingWrapper">
  981. <div class="LoadingThrobber">
  982. <div class="Bar Bar1"></div>
  983. <div class="Bar Bar2"></div>
  984. <div class="Bar Bar3"></div>
  985. </div>
  986. </div>
  987. </div>
  988. <div>
  989. <div id="asf_stm_messagebox" class="profile_badges_header">
  990. <div id="asf_stm_message" class="profile_badges_header_title" style="text-align: center;">Initialization</div>
  991. </div>
  992. </div>
  993. <div style="width: 100%;">
  994. <div id="asf_stm_stop" class="btn_darkred_white_innerfade btn_medium_thin" style="float: right;margin-top: -12px;margin-left: 10px;" title="Stop scan">
  995. <span>🛑</span>
  996. </div>
  997. <div style="width: auto;overflow: hidden;" class="profile_xp_block_remaining_bar">
  998. <div id="asf_stm_progress" class="profile_xp_block_remaining_bar_progress" style="width: 100%;">
  999. </div>
  1000. </div>
  1001. </div>
  1002. </div>
  1003. <div id="asf_stm_filters" style="position: fixed; z-index: 1000; right: 5px; bottom: 45px; transition-duration: 500ms;
  1004. transition-timing-function: ease; margin-right: -50%; padding: 5px; max-width: 40%; display: inline-block; border-radius: 2px;
  1005. background:${filterBackgroundColor}; color: #67c1f5;">
  1006. <div style="white-space: nowrap;">Select:
  1007. <a id="asf_stm_filter_all" class="commentthread_pagelinks">
  1008. all
  1009. </a>
  1010. <a id="asf_stm_filter_none" class="commentthread_pagelinks">
  1011. none
  1012. </a>
  1013. <a id="asf_stm_filter_invert" class="commentthread_pagelinks">
  1014. invert
  1015. </a>
  1016. </div>
  1017. <hr />
  1018. <div id="asf_stm_filters_body">
  1019. <span id="asf_stm_placeholder" style="margin-right: 15px;">No matches to filter</span>
  1020. </div>
  1021. </div>
  1022. <div style="position: fixed;z-index: 1000;right: 5px;bottom: 5px;" id="asf_stm_filters_button_div">
  1023. <a id="asf_stm_filters_button" class="btnv6_blue_hoverfade btn_medium">
  1024. <span>Filters</span>
  1025. </a>
  1026. </div>
  1027. `;
  1028. document.getElementById("asf_stm_stop").addEventListener("click", stopButtonEvent, false);
  1029. document.getElementById("asf_stm_filters_body").addEventListener("change", filterEventHandler);
  1030. document.getElementById("asf_stm_filter_all").addEventListener("click", filterSwitchesHandler);
  1031. document.getElementById("asf_stm_filter_none").addEventListener("click", filterSwitchesHandler);
  1032. document.getElementById("asf_stm_filter_invert").addEventListener("click", filterSwitchesHandler);
  1033. document.getElementById("asf_stm_filters_button").addEventListener("click", filtersButtonEvent, false);
  1034. maxPages = 1;
  1035. stop = false;
  1036. myBadges.length = 0;
  1037. getBadges(1);
  1038. }
  1039.  
  1040. function fetchBots() {
  1041. let requestUrl = "https://asf.justarchi.net/Api/Listing/Bots";
  1042. let requestFunc;
  1043. if (typeof (GM_xmlhttpRequest) !== "function") {
  1044. requestFunc = GM.xmlHttpRequest.bind(GM);
  1045. } else {
  1046. requestFunc = GM_xmlhttpRequest;
  1047. }
  1048. requestFunc({
  1049. method: "GET",
  1050. url: requestUrl,
  1051. onload: function(response) {
  1052. if (response.status != 200) {
  1053. disableButton()
  1054. document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
  1055. debugPrint("can't fetch list of bots, ERROR="+response.status);
  1056. debugPrint(JSON.stringify(response));
  1057. return;
  1058. }
  1059. try {
  1060. let re = /("SteamID":)(\d+)/g;
  1061. let fixedJson = response.response.replace(re, "$1\"$2\""); //because fuck js
  1062. bots = JSON.parse(fixedJson);
  1063. bots.cacheTime = Date.now();
  1064. if (bots.Success) {
  1065. //bots.filter(bot=>bot.matchable_cards===1||bot.matchable_foil_cards===1); //I don't think this is really needed
  1066. bots.Result.sort(function(a, b) { //sort received array as I like it. TODO: sort according to settings
  1067. let result = b.MatchEverything - a.MatchEverything; //bots with MatchEverything go first
  1068. if (result === 0) {
  1069. result = b.TotalGamesCount - a.TotalGamesCount; //then by TotalGamesCount descending
  1070. }
  1071. if (result === 0) {
  1072. result = b.TotalItemsCount - a.TotalItemsCount; //then by TotalItemsCounts descending
  1073. }
  1074. return result;
  1075. });
  1076. debugPrint("found total " + bots.Result.length + " bots");
  1077.  
  1078. localStorage.setItem("Ryzhehvost.ASF.STM.BotCache",JSON.stringify(bots));
  1079. buttonPressedEvent();
  1080. } else {
  1081. //ASF backend does not indicate success
  1082. disableButton()
  1083. document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots, try later");
  1084. debugPrint("can't fetch list of bots");
  1085. debugPrint(bots.Message);
  1086. debugPrint(JSON.stringify(response));
  1087. return;
  1088. }
  1089. return;
  1090. } catch (e) {
  1091. disableButton()
  1092. document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots, try later");
  1093. debugPrint("can't fetch list of bots");
  1094. debugPrint(e);
  1095. debugPrint(JSON.stringify(response));
  1096. return;
  1097. }
  1098. },
  1099. onerror: function(response) {
  1100. disableButton()
  1101. document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
  1102. debugPrint("can't fetch list of bots");
  1103. debugPrint(JSON.stringify(response));
  1104. },
  1105. onabort: function(response) {
  1106. disableButton()
  1107. document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
  1108. debugPrint("can't fetch list of bots - aborted");
  1109. debugPrint(JSON.stringify(response));
  1110. },
  1111. ontimeout: function(response) {
  1112. disableButton()
  1113. document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
  1114. debugPrint("can't fetch list of bots - timeout");
  1115. debugPrint(JSON.stringify(response));
  1116. }
  1117. });
  1118. }
  1119.  
  1120. if (document.getElementsByClassName("badge_details_set_favorite").length != 0) {
  1121. let profileRegex = /http[s]?:\/\/steamcommunity.com\/(.*)\/badges.*/g;
  1122. let result = profileRegex.exec(document.location);
  1123. if (result) {
  1124. myProfileLink = result[1];
  1125. } else { //should never happen, but whatever.
  1126. myProfileLink = "my"
  1127. }
  1128.  
  1129. debugPrint(profileRegex);
  1130.  
  1131. let botCache = JSON.parse(localStorage.getItem("Ryzhehvost.ASF.STM.BotCache"));
  1132. if (botCache === null || botCache.cacheTime === undefined || botCache.cacheTime === null || botCache.cacheTime + botCacheTime < Date.now()) {
  1133. botCache = null;
  1134. debugPrint("Bot cache invalidated");
  1135. } else {
  1136. bots = botCache;
  1137. }
  1138.  
  1139. let buttonDiv = document.createElement("div");
  1140. buttonDiv.setAttribute("class", "profile_small_header_additional");
  1141. buttonDiv.setAttribute("style", "margin-top: 40px;");
  1142. buttonDiv.setAttribute("id", "asf_stm_button_div");
  1143. buttonDiv.setAttribute("title", "Scan ASF STM");
  1144. let button = document.createElement("a");
  1145. button.setAttribute("class", "btnv6_blue_hoverfade btn_medium");
  1146. button.setAttribute("id", "asf_stm_button");
  1147. button.appendChild(document.createElement("span"));
  1148. button.firstChild.appendChild(document.createTextNode("Scan ASF STM"));
  1149. buttonDiv.appendChild(button);
  1150. let anchor = document.getElementsByClassName("profile_small_header_texture")[0];
  1151. anchor.appendChild(buttonDiv);
  1152. enableButton();
  1153. }
  1154. })();