// ==UserScript==
// @name ASF STM
// @namespace https://gf.qytechs.cn/users/2205
// @description ASF bot list trade matcher
// @description:vi Trình khớp lệnh giao dịch danh sách bot ASF
// @license Apache-2.0
// @author Rudokhvist
// @match *://steamcommunity.com/id/*/badges
// @match *://steamcommunity.com/id/*/badges/
// @match *://steamcommunity.com/profiles/*/badges
// @match *://steamcommunity.com/profiles/*/badges/
// @match *://steamcommunity.com/tradeoffer/new/*source=asfstm*
// @version 3.3
// @connect asf.justarchi.net
// @grant GM.xmlHttpRequest
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function () {
"use strict";
let myProfileLink = "";
let errors = 0;
let bots = null;
let myBadges = [];
let botBadges = [];
let maxPages;
let stop = false;
let botCacheTime = 5 * 60000;
let globalSettings = null;
let blacklist = [];
let defaultSettings = {
anyBots:true,
fairBots:true,
sortByName: true,
sortBotsBy: ["MatchEverythingFirst","TotalGamesCountDesc","TotalItemsCountDesc","TotalInventoryCountAsc", "None"],
botMinItems: 0,
botMaxItems: 0,
weblimiter:300,
errorLimiter:30000,
debug:false,
maxErrors:3,
filterBackgroundColor:"rgba(23,26,33,0.8)",
// for trade offer
tradeMessage:'ASF STM Matcher',
autoSend:false,
doAfterTrade:'NOTHING',
order:'AS_IS',
};
//styles
const css = `
#asf_stm_filters_body {
max-height: calc(100vh - 95px);
overflow-y: auto;
}
.asf_stm_tabs{
width: 600px;
display: block;
margin: 40px auto;
position: relative;
}
.asf_stm_tabs .asf_stm_tab{
float: left;
display: block;
}
.asf_stm_tabs .asf_stm_tab>input[type="radio"] {
position: absolute;
top: -9999px;
left: -9999px;
}
.asf_stm_tabs .asf_stm_tab>label {
display: block;
padding: 6px 21px;
cursor: pointer;
position: relative;
color: #FFF;
background: #4A83FD;
}
.asf_stm_tabs .asf_stm_content {
display: none;
overflow: hidden;
width: 630px;
height: 380px;
padding: 5px;
position: absolute;
top: 27px;
left: 0;
background: #303030;
color: #DFDFDF;
}
.asf_stm_tabs>.asf_stm_tab>[id^="asf_stm_tab"]:checked + label {
top: 0;
background: #303030;
color: #F5F5F5;
}
.asf_stm_tabs>.asf_stm_tab>[id^="asf_stm_tab"]:checked ~ [id^="asf_stm_tab-content"] {
display: block;
}
textarea {
resize: none;
}
`;
function debugTime(name) {
if (globalSettings.debug) {
console.time(name);
}
}
function debugTimeEnd(name) {
if (globalSettings.debug) {
console.timeEnd(name);
}
}
function debugPrint(msg) {
if (globalSettings.debug) {
console.log(new Date().toLocaleTimeString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", fractionalSecondDigits: 3 }) + " : " + msg);
}
}
function deepClone(object) {
return JSON.parse(JSON.stringify(object));
}
function getPartner(str) {
if (typeof BigInt !== "undefined") {
return (BigInt(str) % BigInt(4294967296)).toString(); // eslint-disable-line
} else {
let result = 0;
for (let i = 0; i < str.length; i++) {
result = (result * 10 + Number(str[i])) % 4294967296;
}
return result.toString();
}
}
function arrayToText(array) {
return array.join(",\n");
}
function textToArray(text) {
let res = [];
text.split(",").forEach(function(elem) {
if (/^\d+$/.test(elem.trim())) {
res.push(elem.trim());
}
});
return res;
}
function hexToRgba(hex) {
return 'rgba('+[Number("0x"+hex.substring(1,3)), Number("0x"+hex.substring(3,5)), Number("0x"+hex.substring(5,7))].join(',')+',1)';
}
function rgbaToHex(rgba) {
let re=/rgba\(([.\d]+),([.\d]+),([.\d]+),([.\d]+)\)/g;
let result = re.exec(rgba);
if (result === null || result.length != 5) {
debugPrint("failed to parse color!");
return ["#171a21",0.8];
}
return ["#"+Number(result[1]).toString(16)+Number(result[2]).toString(16)+Number(result[3]).toString(16),Number(result[4])];
}
function mixAlpha(rgba,alpha) {
let re = /(rgba\([.\d]+,[.\d]+,[.\d]+,)([.\d]+)\)/g;
let result = re.exec(rgba);
if (result) {
return result[1]+alpha+")";
}
debugPrint("failed to mix alpha!");
return rgba;
}
function ShowConfigDialog() {
let filterBG = rgbaToHex(globalSettings.filterBackgroundColor);
let configDialogTemplate = `
<div style="height:420px; margin-top:-20px; font-size:12px;margin-left: -20px;width: 620px;" >
<ul style="margin: 0;padding: 0;" class="asf_stm_tabs">
<li class="asf_stm_tab">
<input name="asf_stm_tabs" checked="checked"
id="asf_stm_tab1" type="radio"><label
for="asf_stm_tab1">Matcher</label>
<div id="asf_stm_tab-content1" class="asf_stm_content">
<fieldset style="padding-top: 0px;"><legend>BOTS TO MATCH</legend>
<div style="margin-bottom: 6px;"> Match with "Any"
bots: <input style="background-color: #171d25; color: white;"
id="anyBots" ${globalSettings.anyBots ? "checked=\"checked\"" : ""} type="checkbox"> Match
with "Fair"
bots: <input style="background-color: #171d25; color: white;"
id="fairBots" ${globalSettings.fairBots ? "checked=\"checked\"" : ""} type="checkbox"><br>
</div>
<div style="margin-bottom: 6px;"> Minimum
items: <input min=0 style="background-color: #171d25; color: white;"
id="botMinItems" value=${globalSettings.botMinItems} type="number"> Maximum
items: <input min=0 style="background-color: #171d25; color: white;"
id="botMaxItems" value=${globalSettings.botMaxItems} type="number"><a class="tooltip hover_tooltip" data-tooltip-text="Don't match with bots
that has less or more than required limit of items in steam inventory. 0 means no limit on number of items">
<img src="https://store.cloudflare.steamstatic.com/public/shared/images/ico/icon_questionmark.png"></a>
<br>
</div>
<div style="margin-bottom: 6px;">
<span style="width: 80px; display: inline-block;">Sort bots by:</span>
<select style="background-color: #171d25; color: white;"
id="sortBotsBy0">
<option value="MatchEverythingFirst" ${globalSettings.sortBotsBy[0]==="MatchEverythingFirst" ? "selected=\"selected\"" : ""}>"Any"
bots first</option>
<option value="MatchEverythingLast" ${globalSettings.sortBotsBy[0]==="MatchEverythingLast" ? "selected=\"selected\"" : ""}>"Any" bots last</option>
<option value="TotalGamesCountDesc" ${globalSettings.sortBotsBy[0]==="TotalGamesCountDesc" ? "selected=\"selected\"" : ""}>Total games count, descending</option>
<option value="TotalGamesCountAsc" ${globalSettings.sortBotsBy[0]==="TotalGamesCountAsc" ? "selected=\"selected\"" : ""}>Total games count, ascending</option>
<option value="TotalItemsCountDesc" ${globalSettings.sortBotsBy[0]==="TotalItemsCountDesc" ? "selected=\"selected\"" : ""}>Total matchable items count,
descending</option>
<option value="TotalItemsCountAsc" ${globalSettings.sortBotsBy[0]==="TotalItemsCountAsc" ? "selected=\"selected\"" : ""}>Total matchable items count,
ascending</option>
<option value="TotalInventoryCountDesc" ${globalSettings.sortBotsBy[0]==="TotalInventoryCountDesc" ? "selected=\"selected\"" : ""}>Total inventory count, descending</option>
<option value="TotalInventoryCountAsc" ${globalSettings.sortBotsBy[0]==="TotalInventoryCountAsc" ? "selected=\"selected\"" : ""}>Total inventory count, ascending</option>
<option value="None" ${globalSettings.sortBotsBy[0]==="None" ? "selected=\"selected\"" : ""}>None</option>
</select>
</div>
<div style="margin-bottom: 6px;"><span
style="width: 80px; display: inline-block;"> …then by:</span>
<select style="background-color: #171d25; color: white;" id="sortBotsBy1">
<option value="MatchEverythingFirst" ${globalSettings.sortBotsBy[1]==="MatchEverythingFirst" ? "selected=\"selected\"" : ""}>"Any"
bots first</option>
<option value="MatchEverythingLast" ${globalSettings.sortBotsBy[1]==="MatchEverythingLast" ? "selected=\"selected\"" : ""}>"Any" bots last</option>
<option value="TotalGamesCountDesc" ${globalSettings.sortBotsBy[1]==="TotalGamesCountDesc" ? "selected=\"selected\"" : ""}>Total games count, descending</option>
<option value="TotalGamesCountAsc" ${globalSettings.sortBotsBy[1]==="TotalGamesCountAsc" ? "selected=\"selected\"" : ""}>Total games count, ascending</option>
<option value="TotalItemsCountDesc" ${globalSettings.sortBotsBy[1]==="TotalItemsCountDesc" ? "selected=\"selected\"" : ""}>Total matchable items count,
descending</option>
<option value="TotalItemsCountAsc" ${globalSettings.sortBotsBy[1]==="TotalItemsCountAsc" ? "selected=\"selected\"" : ""}>Total matchable items count,
ascending</option>
<option value="TotalInventoryCountDesc" ${globalSettings.sortBotsBy[1]==="TotalInventoryCountDesc" ? "selected=\"selected\"" : ""}>Total inventory count, descending</option>
<option value="TotalInventoryCountAsc" ${globalSettings.sortBotsBy[1]==="TotalInventoryCountAsc" ? "selected=\"selected\"" : ""}>Total inventory count, ascending</option>
<option value="None" ${globalSettings.sortBotsBy[1]==="None" ? "selected=\"selected\"" : ""}>None</option>
</select>
</div>
<div style="margin-bottom: 6px;"><span
style="width: 80px; display: inline-block;">…then by:</span>
<select style="background-color: #171d25; color: white;"
id="sortBotsBy2">
<option value="MatchEverythingFirst" ${globalSettings.sortBotsBy[2]==="MatchEverythingFirst" ? "selected=\"selected\"" : ""}>"Any"
bots first</option>
<option value="MatchEverythingLast" ${globalSettings.sortBotsBy[2]==="MatchEverythingLast" ? "selected=\"selected\"" : ""}>"Any" bots last</option>
<option value="TotalGamesCountDesc" ${globalSettings.sortBotsBy[2]==="TotalGamesCountDesc" ? "selected=\"selected\"" : ""}>Total games count, descending</option>
<option value="TotalGamesCountAsc" ${globalSettings.sortBotsBy[2]==="TotalGamesCountAsc" ? "selected=\"selected\"" : ""}>Total games count, ascending</option>
<option value="TotalItemsCountDesc" ${globalSettings.sortBotsBy[2]==="TotalItemsCountDesc" ? "selected=\"selected\"" : ""}>Total matchable items count,
descending</option>
<option value="TotalItemsCountAsc" ${globalSettings.sortBotsBy[2]==="TotalItemsCountAsc" ? "selected=\"selected\"" : ""}>Total matchable items count,
ascending</option>
<option value="TotalInventoryCountDesc" ${globalSettings.sortBotsBy[2]==="TotalInventoryCountDesc" ? "selected=\"selected\"" : ""}>Total inventory count, descending</option>
<option value="TotalInventoryCountAsc" ${globalSettings.sortBotsBy[2]==="TotalInventoryCountAsc" ? "selected=\"selected\"" : ""}>Total inventory count, ascending</option>
<option value="None" ${globalSettings.sortBotsBy[2]==="None" ? "selected=\"selected\"" : ""}>None</option>
</select>
</div>
<div style="margin-bottom: 6px;"><span
style="width: 80px; display: inline-block;">…then by:</span>
<select style="background-color: #171d25; color: white;"
id="sortBotsBy3">
<option value="MatchEverythingFirst" ${globalSettings.sortBotsBy[3]==="MatchEverythingFirst" ? "selected=\"selected\"" : ""}>"Any"
bots first</option>
<option value="MatchEverythingLast" ${globalSettings.sortBotsBy[3]==="MatchEverythingLast" ? "selected=\"selected\"" : ""}>"Any" bots last</option>
<option value="TotalGamesCountDesc" ${globalSettings.sortBotsBy[3]==="TotalGamesCountDesc" ? "selected=\"selected\"" : ""}>Total games count, descending</option>
<option value="TotalGamesCountAsc" ${globalSettings.sortBotsBy[3]==="TotalGamesCountAsc" ? "selected=\"selected\"" : ""}>Total games count, ascending</option>
<option value="TotalItemsCountDesc" ${globalSettings.sortBotsBy[3]==="TotalItemsCountDesc" ? "selected=\"selected\"" : ""}>Total matchable items count,
descending</option>
<option value="TotalItemsCountAsc" ${globalSettings.sortBotsBy[3]==="TotalItemsCountAsc" ? "selected=\"selected\"" : ""}>Total matchable items count,
ascending</option>
<option value="TotalInventoryCountDesc" ${globalSettings.sortBotsBy[3]==="TotalInventoryCountDesc" ? "selected=\"selected\"" : ""}>Total inventory count, descending</option>
<option value="TotalInventoryCountAsc" ${globalSettings.sortBotsBy[3]==="TotalInventoryCountAsc" ? "selected=\"selected\"" : ""}>Total inventory count, ascending</option>
<option value="None" ${globalSettings.sortBotsBy[3]==="None" ? "selected=\"selected\"" : ""}>None</option>
</select>
</div>
</fieldset>
<fieldset style="padding-top: 0px;"><legend>INTERFACE</legend>
<div style="margin-bottom: 6px;"> Game filter pop-up
background color: <input
style="background-color: #171d25; color: white; height: 20px; padding: 2px;"
id="filterBackgroundColor" value="${filterBG[0]}" type="color"> opacity: <input
style="height: 4px;"
id="filterBackgroundAlpha" min=0 max=1
step=0.01 value=${filterBG[1]} type="range"><br>
</div>
<div style="margin-bottom: 6px;"> Sort results by game
name: <input
style="border: medium none transparent; background-color: #171d25; color: white;"
id="sortByName" ${globalSettings.sortByName ? "checked=\"checked\"" : ""} type="checkbox">
</div>
</fieldset>
<fieldset style="padding-top: 0px;"><legend>SETTINGS</legend>
<fieldset style="padding-top: 0px; float: right; width:200px;"><legend>DEVELOPER</legend>
<div style="margin-bottom: 6px;">Debug:
<input style="background-color: #171d25; color: white;"
id="debug" type="checkbox" ${globalSettings.debug ? "checked=\"checked\"" : ""} >
<a class="tooltip hover_tooltip" data-tooltip-text="Enable additional output to console">
<img src="https://store.cloudflare.steamstatic.com/public/shared/images/ico/icon_questionmark.png"></a>
</div>
</fieldset>
<div style="margin-bottom: 6px;"> Web limiter delay,
ms: <input class="price_option_input"
style="border: medium none transparent; background-color: #171d25; color: white;"
id="weblimiter" value= ${globalSettings.weblimiter} min=0 type="number"></div>
<div style="margin-bottom: 6px;"> Delay on error,
ms: <input class="price_option_input"
style="background-color: #171d25; color: white;"
id="errorLimiter" value=${globalSettings.errorLimiter} min=0 type="number"></div>
<div style="margin-bottom: 6px;"> Max errors: <input
class="price_option_input"
style="background-color: #171d25; color: white;"
id="maxErrors" value=${globalSettings.maxErrors} min=0 type="number"></div>
</div>
</fieldset>
</li>
<li class="asf_stm_tab">
<input name="asf_stm_tabs" id="asf_stm_tab2"
type="radio"><label for="asf_stm_tab2">Trade
helper</label>
<div id="asf_stm_tab-content2" class="asf_stm_content">
<br>
<fieldset><legend>TRADE OFFER MESSAGE</legend>
<textarea style="background-color: #171d25; color: white;" id="tradeMessage" name="tradeMessage" rows="4"
cols="60">${globalSettings.tradeMessage}</textarea>
<a class="tooltip hover_tooltip" data-tooltip-text="Custom text that will be included automatically with your
trade offers created through STM while using this userscript. To remove this functionality, simply delete the text.">
<img src="https://store.cloudflare.steamstatic.com/public/shared/images/ico/icon_questionmark.png"></a>
</fieldset>
<br>
<fieldset><legend>ACTION AFTER TRADE
</legend>
<label for="after-trade">After
trade...</label>
<select style="background-color: #171d25; color: white;" name="after-trade" id="doAfterTrade">
<option value="NOTHING" ${globalSettings.doAfterTrade == "NOTHING" ? "selected=\"selected\"" : ""}>Do
Nothing</option>
<option value="CLOSE_WINDOW" ${globalSettings.doAfterTrade == "CLOSE_WINDOW" ? "selected=\"selected\"" : ""}>Close window</option>
<option value="CLICK_OK" ${globalSettings.doAfterTrade == "CLICK_OK" ? "selected=\"selected\"" : ""}>Click OK</option>
</select>
<a class="tooltip hover_tooltip" data-tooltip-html="
<p>Determines what happens when you complete a trade offer.</p>
<ul>
<li><strong>Do nothing</strong>: Will do
nothing more than the normal behavior.</li>
<li><strong>Close window</strong>: Will close
the window after the trade offer is sent.</li>
<li><strong>Click OK</strong>: Will redirect
you to the trade offers recap page.</li>
</ul>">
<img src="https://store.cloudflare.steamstatic.com/public/shared/images/ico/icon_questionmark.png"></a>
</fieldset>
<br>
<fieldset><legend>CARDS OFFER
</legend>
<label for="cards-order">Cards
order</label>
<select style="background-color: #171d25; color: white;" class="form-control" name="cards-order"
id="order">
<option value="SORT" ${globalSettings.order == "SORT" ? "selected=\"selected\"" : ""}>Sorted</option>
<option value="RANDOM" ${globalSettings.order == "RANDOM" ? "selected=\"selected\"" : ""}>Random</option>
<option value="AS_IS" ${globalSettings.order == "AS_IS" ? "selected=\"selected\"" : ""}>As is</option>
</select>
<a class="tooltip hover_tooltip" data-tooltip-html="
<p>Determines which card is added to trade.</p>
<ul>
<li><strong>Sorted</strong>: Will sort cards by
their IDs before adding to trade. If you make several trade offers with
the same card and one of them is accepted, the rest will have message
"cards unavilable to trade".</li>
<li><strong>Random</strong>: Will add cards to
trade randomly. If you make several trade offers and one of them is
accepted, only some of them will be unavilable for trade.</li>
<li><strong>As is</strong>: Script doesn't
change anything in order. Results vary depending on browser, steam
servers, weather...</li>
</ul>">
<img src="https://store.cloudflare.steamstatic.com/public/shared/images/ico/icon_questionmark.png"></a>
</fieldset>
<br>
<fieldset><legend>AUTO-SEND TRADE OFFER
</legend>
<label for="auto-send"><input style="background-color: #171d25; color: white;" name="auto-send"
id="autoSend" value="1" type="checkbox" ${globalSettings.autoSend ? "checked=\"checked\"" : ""}>
Enable</label>
<a class="tooltip hover_tooltip" data-tooltip-text="Makes it possible for the script to automatically send
trade offers without any action on your side. This is not recommended
as you should always check your trade offers, but, well, this is a
possible thing. Please note that incomplete trade offers (missing
cards, ...) won't be sent automatically even when this parameter is set
to true.">
<img src="https://store.cloudflare.steamstatic.com/public/shared/images/ico/icon_questionmark.png"></a>
</fieldset>
</div>
</li>
<li class="asf_stm_tab">
<input name="asf_stm_tabs" id="asf_stm_tab3"
type="radio"><label for="asf_stm_tab3">Blacklist</label>
<div id="asf_stm_tab-content3" class="asf_stm_content">
<div class="title_text profile_xp_block_remaining"><h1>Comma-separated list of ignored steamIDs</h1><div><div>
<textarea style="background-color: #171d25; color: white;" id="blacklist" name="Blacklist" rows="17"
cols="63">${arrayToText(blacklist)}</textarea></div>
</li>
</ul>
</div>
`;
let templateElement = document.createElement("template");
templateElement.innerHTML = configDialogTemplate.trim();
let configDialog = templateElement.content.firstChild;
unsafeWindow.ShowConfirmDialog('ASF STM Configuration', configDialog, "Save","Cancel","Reset").done(function(button) {
if (button === 'OK') {
globalSettings.anyBots = configDialog.querySelector("#anyBots").checked;
globalSettings.fairBots = configDialog.querySelector("#fairBots").checked;
globalSettings.sortByName = configDialog.querySelector("#sortByName").checked;
let newsortBotsBy = [];
configDialog.querySelectorAll("[id^=sortBotsBy").forEach(function(elem){
newsortBotsBy.push(elem.selectedOptions[0].value);
});
globalSettings.sortBotsBy = newsortBotsBy;
let newbotMinItems = Number(configDialog.querySelector("#botMinItems").value);
globalSettings.botMinItems = isNaN(newbotMinItems) ? globalSettings.botMinItems : newbotMinItems;
let newbotMaxItems = Number(configDialog.querySelector("#botMaxItems").value);
globalSettings.botMaxItems = isNaN(newbotMaxItems) ? globalSettings.botMaxItems : newbotMaxItems;
let newweblimiter = Number(configDialog.querySelector("#weblimiter").value);
globalSettings.weblimiter = isNaN(newweblimiter) ? globalSettings.weblimiter : newweblimiter;
let newerrorLimiter = Number(configDialog.querySelector("#errorLimiter").value);
globalSettings.errorLimiter = isNaN(newerrorLimiter) ? globalSettings.errorLimiter : newerrorLimiter;
globalSettings.debug = configDialog.querySelector("#debug").checked;
let newmaxErrors = Number(configDialog.querySelector("#maxErrors").value);
globalSettings.maxErrors = isNaN(newmaxErrors) ? globalSettings.maxErrors : newmaxErrors;
globalSettings.filterBackgroundColor = mixAlpha(hexToRgba(configDialog.querySelector("#filterBackgroundColor").value),
configDialog.querySelector("#filterBackgroundAlpha").value);
globalSettings.tradeMessage = configDialog.querySelector("#tradeMessage").value;
globalSettings.autoSend = configDialog.querySelector("#autoSend").checked;
globalSettings.doAfterTrade = configDialog.querySelector("#doAfterTrade").selectedOptions[0].value;
globalSettings.order = configDialog.querySelector("#order").selectedOptions[0].value;
blacklist = textToArray(configDialog.querySelector("#blacklist").value);
SaveConfig();
unsafeWindow.ShowConfirmDialog('CONFIRMATION', 'Some changes may not work prior to page reload. Do you want to reload the page?').done(function(){
document.location.reload()
});
} else {
unsafeWindow.ShowConfirmDialog('CONFIRMATION', 'Are you sure you want to restore default settings?').done(function(){
ResetConfig();
SaveConfig();
unsafeWindow.ShowConfirmDialog('CONFIRMATION', 'Some changes may not work prior to page reload. Do you want to reload the page?').done(function(){
document.location.reload()
});
});
}
});
}
function ResetConfig() {
//we won't clear blacklist here!
globalSettings = deepClone(defaultSettings);
}
function SaveConfig() {
localStorage.setItem("Ryzhehvost.ASF.STM.Settings",JSON.stringify(globalSettings));
localStorage.setItem("Ryzhehvost.ASF.STM.Blacklist",JSON.stringify(blacklist));
}
function LoadConfig() {
globalSettings = JSON.parse(localStorage.getItem("Ryzhehvost.ASF.STM.Settings"));
blacklist = JSON.parse(localStorage.getItem("Ryzhehvost.ASF.STM.Blacklist"));
if (globalSettings === null) {
ResetConfig();
}
if (blacklist === null) {
blacklist = [];
}
//vaildate config
Object.keys(defaultSettings).forEach(function(key){
if (!globalSettings.hasOwnProperty(key)){
globalSettings[key] = defaultSettings[defaultSettings];
}
});
}
function enableButton() {
let buttonDiv = document.getElementById("asf_stm_button_div");
buttonDiv.setAttribute("class", "profile_small_header_additional");
buttonDiv.setAttribute("title", "Scan ASF STM");
let button = document.getElementById("asf_stm_button");
button.addEventListener("click", buttonPressedEvent, false);
}
function disableButton() {
let buttonDiv = document.getElementById("asf_stm_button_div");
buttonDiv.setAttribute("class", "profile_small_header_additional btn_disabled");
buttonDiv.setAttribute("title", "Scan is in process");
let button = document.getElementById("asf_stm_button");
button.removeEventListener("click", buttonPressedEvent, false);
}
function updateMessage(text) {
let message = document.getElementById("asf_stm_message");
message.textContent = text;
}
function hideMessage() {
let messageBox = document.getElementById("asf_stm_messagebox");
messageBox.setAttribute("style", "display: none;");
}
function hideThrobber() {
let throbber = document.getElementById("throbber");
throbber.setAttribute("style", "display: none;");
}
function updateProgress(index) {
let bar = document.getElementById("asf_stm_progress");
let progress = 100 * ((index + 1) / bots.Result.length);
bar.setAttribute("style", "width: " + progress + "%;");
}
function blacklistEventHandler(event) {
let steamID = event.currentTarget.id.split("_")[1];
if (blacklist.includes(steamID)) {
return;
}
unsafeWindow.ShowConfirmDialog('CONFIRMATION', `Are you sure you want to blacklist bot ${steamID} ?`).done(function(){
blacklist.push(steamID);
SaveConfig();
});
}
function populateCards(item) {
let nameList = "";
let htmlCards = "";
for (let j = 0; j < item.cards.length; j++) {
let itemIcon = item.cards[j].iconUrl;
let itemName = item.cards[j].item;
for (let k = 0; k < item.cards[j].count; k++) {
if (nameList != "") {
nameList += ";";
}
nameList += encodeURIComponent(item.cards[j].iconUrl.substring(item.cards[j].iconUrl.length - 5) + "." + item.appId + "-" + item.cards[j].item);
let cardTemplate = `
<div class="showcase_slot">
<img class="image-container" src="${itemIcon}/98x115">
<div class="commentthread_subscribe_hint" style="width: 98px;">${itemName}</div>
</div>
`;
htmlCards += cardTemplate;
}
}
return {
htmlCards: htmlCards,
nameList: nameList
};
}
function getNames(item) {
let names = "";
for (let j = 0; j < item.cards.length; j++) {
for (let k = 0; k < item.cards[j].count; k++) {
if (names != "") {
names += ";";
}
names += encodeURIComponent(item.cards[j].iconUrl.substring(item.cards[j].iconUrl.length - 5) + "." + item.appId + "-" + item.cards[j].item);
}
}
return names;
}
function updateTrade(row) {
let index = row.id.split("_")[1];
let tradeLink = row.getElementsByClassName("full_trade_url")[0];
let splitUrl = tradeLink.href.split("&");
let them = "";
let you = "";
for (let i = 0; i < bots.Result[index].itemsToSend.length; i++) {
let appId = bots.Result[index].itemsToSend[i].appId;
let checkBox = document.getElementById("astm_" + appId);
if (checkBox.checked) {
if (you != "") {
you += ";";
}
you = you + getNames(bots.Result[index].itemsToSend[i]);
if (them != "") {
them += ";";
}
them = them + getNames(bots.Result[index].itemsToReceive[i]);
}
}
splitUrl[3] = "them=" + them;
splitUrl[4] = "you=" + you;
tradeLink.href = splitUrl.join("&");
}
function checkRow(row) {
debugPrint("checkRow");
let matches = row.getElementsByClassName("badge_row");
let visible = false;
for (let i = 0; i < matches.length; i++) {
if (matches[i].parentElement.style.display != "none") {
visible = true;
break;
}
}
if (visible) {
row.style.display = "block";
updateTrade(row);
} else {
row.style.display = "none";
}
}
function addMatchRow(index) {
debugPrint("addMatchRow " + index);
let itemsToSend = bots.Result[index].itemsToSend;
let itemsToReceive = bots.Result[index].itemsToReceive;
// sort by game name
function compareNames(a, b) {
const nameA = a.title;
const nameB = b.title;
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
}
if (globalSettings.sortByName) {
itemsToSend.sort(compareNames);
itemsToReceive.sort(compareNames);
}
let tradeUrl = "https://steamcommunity.com/tradeoffer/new/?partner=" + getPartner(bots.Result[index].SteamID) + "&token=" + bots.Result[index].TradeToken + "&source=asfstm";
let globalYou = "";
let globalThem = "";
let matches = "";
let any = "";
if (bots.Result[index].MatchEverything == 1) {
any = ` <sup><span class="avatar_block_status_in-game" style="font-size: 8px; cursor:help" title="This bots trades for any cards within same set"> ANY </span></sup>`;
}
for (let i = 0; i < itemsToSend.length; i++) {
let appId = itemsToSend[i].appId;
let itemToReceive = itemsToReceive.find((a) => a.appId == appId);
let gameName = itemsToSend[i].title;
let display = "inline-block";
//remove placeholder
let filterWidget = document.getElementById("asf_stm_filters_body");
let placeholder = document.getElementById("asf_stm_placeholder");
if (placeholder != null) {
placeholder.remove();
}
//add filter
let checkBox = document.getElementById("astm_" + appId);
if (checkBox == null) {
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>`;
let spanTemplate = document.createElement("template");
spanTemplate.innerHTML = newFilter.trim();
filterWidget.appendChild(spanTemplate.content.firstChild);
} else {
if (checkBox.checked == false) {
display = "none";
}
}
let sendResult = populateCards(itemsToSend[i]);
let receiveResult = populateCards(itemToReceive);
let tradeUrlApp = tradeUrl + "&them=" + receiveResult.nameList + "&you=" + sendResult.nameList;
let matchTemplate = `
<div class="asf_stm_appid_${appId}" style="display:${display}">
<div class="badge_row is_link goo_untradable_note showcase_slot">
<div class="notLoggedInText">
<img style="background-color: var(--gpStoreDarkerGrey);" height=69 alt="${gameName}" src="https://steamcdn-a.akamaihd.net/steam/apps/${appId}/capsule_184x69.jpg"
onerror="this.onerror=null;this.src='https://store.akamai.steamstatic.com/public/images/gift/steam_logo_digitalgiftcard.png'">
<div>
<div title="View badge progress for this game">
<a target="_blank" rel="noopener noreferrer" href="https://steamcommunity.com/${myProfileLink}/gamecards/${appId}/">${gameName}</a>
</div>
</div>
<div class="btn_darkblue_white_innerfade btn_medium">
<span>
<a href="${tradeUrlApp}" target="_blank" rel="noopener noreferrer">Offer a trade</a>
</span>
</div>
</div>
<div class="showcase_slot">
<div class="showcase_slot profile_header">
<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>
${sendResult.htmlCards}
</div>
<span class="showcase_slot badge_info_title booster_creator_actions">
<h1>➡</h1>
</span>
</div>
<div class="showcase_slot profile_header">
<div class="badge_info_unlocked profile_xp_block_mid avatar_block_status_online badge_info_title badge_row_overlay ellipsis" style="height: 15px;">
${bots.Result[index].Nickname}
</div>
${receiveResult.htmlCards}
</div>
</div>
</div>
`;
if (checkBox == null || checkBox.checked) {
matches += matchTemplate;
if (globalYou != "") {
globalYou += ";";
}
globalYou += sendResult.nameList;
if (globalThem != "") {
globalThem += ";";
}
globalThem += receiveResult.nameList;
}
}
let tradeUrlFull = tradeUrl + "&them=" + globalThem + "&you=" + globalYou;
let rowTemplate = `
<div id="asfstmbot_${index}" class="badge_row">
<div class="badge_row_inner">
<div class="badge_title_row guide_showcase_contributors">
<div class="badge_title_stats">
<div class="btn_darkblue_white_innerfade btn_medium">
<span>
<a class="full_trade_url" href="${tradeUrlFull}" target="_blank" rel="noopener noreferrer" >Offer a trade for all</a>
</span>
</div>
</div>
<div style="float: left;" class="">
<div class="user_avatar playerAvatar online">
<a target="_blank" rel="noopener noreferrer" href="https://steamcommunity.com/profiles/${bots.Result[index].SteamID}">
<img src="https://avatars.cloudflare.steamstatic.com/${bots.Result[index].AvatarHash === null ? "fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb" : bots.Result[index].AvatarHash}.jpg" />
</a>
</div>
</div>
<div class="badge_title">
<a target="_blank" rel="noopener noreferrer" href="https://steamcommunity.com/profiles/${bots.Result[index].SteamID}">${bots.Result[index].Nickname}</a>${any}
 <span style="color: #8F98A0;">(${bots.Result[index].TotalInventoryCount} items)</span>
 <a id="blacklist_${bots.Result[index].SteamID}" data-tooltip-text="Blacklist this bot" class="tooltip hover_tooltip"><img
src="https://community.cloudflare.steamstatic.com/public/images/skin_1/iconForumBan.png?v=1"></a>
</div>
</div>
<div class="badge_title_rule"></div>
${matches}
</div>
</div>
`;
let template = document.createElement("template");
template.innerHTML = rowTemplate.trim();
let mainContentDiv = document.getElementsByClassName("maincontent")[0];
let newChild = template.content.firstChild;
newChild.querySelector(`#blacklist_${bots.Result[index].SteamID}`).addEventListener("click", blacklistEventHandler, true);
mainContentDiv.appendChild(newChild);
checkRow(newChild);
}
function calcState(badge) {
//state 0 - less than max sets; state 1 - we have max sets, even out the rest, state 2 - all even
debugPrint("maxSets=" + badge.maxSets + " LastSet=" + badge.lastSet +
" Max cards=" + badge.cards[badge.maxCards - 1].count + " Min cards=" + badge.cards[0].count);
if (badge.cards[badge.maxCards - 1].count == badge.maxSets) {
if (badge.cards[0].count == badge.lastSet) {
return 2; //nothing to do
} else {
return 1; //max sets are here, but we can distribute cards further
}
} else {
return 0; //less than max sets
}
}
function compareCards(index, callback) {
let itemsToSend = [];
let itemsToReceive = [];
debugPrint("bot's cards");
debugPrint(JSON.stringify(botBadges));
debugPrint("our cards");
debugPrint(JSON.stringify(myBadges));
for (let i = 0; i < botBadges.length; i++) {
let myBadge = deepClone(myBadges[i]);
let theirBadge = deepClone(botBadges[i]);
let myState = calcState(myBadge);
debugPrint("state=" + myState);
debugPrint("myapp=" + myBadge.appId + " botapp=" + theirBadge.appId);
while (myState < 2) {
let foundMatch = false;
for (let j = 0; j < theirBadge.maxCards; j++) {
//index of card they give
if (theirBadge.cards[j].count > 0) {
//try to match
let myInd = myBadge.cards.findIndex((a) => a.item == theirBadge.cards[j].item); //index of slot where we receive card
if ((myState == 0 && myBadge.cards[myInd].count < myBadge.maxSets) || (myState == 1 && myBadge.cards[myInd].count < myBadge.lastSet)) {
//we need this ^Kfor the Emperor
debugPrint("we need this: " + theirBadge.cards[j].item + " (" + theirBadge.cards[j].count + ")");
//find a card to match.
for (let k = 0; k < myInd; k++) {
//index of card we give
debugPrint("i=" + i + " j=" + j + " k=" + k + " myState=" + myState);
debugPrint("we have this: " + myBadge.cards[k].item + " (" + myBadge.cards[k].count + ")");
if ((myState == 0 && myBadge.cards[k].count > myBadge.maxSets) || (myState == 1 && myBadge.cards[k].count > myBadge.lastSet)) {
//that's fine for us
debugPrint("it's a good trade for us");
let theirInd = theirBadge.cards.findIndex((a) => a.item == myBadge.cards[k].item); //index of slot where they will receive card
if (bots.Result[index].MatchEverything == 0) {
//make sure it's neutral+ for them
if (theirBadge.cards[theirInd].count >= theirBadge.cards[j].count) {
debugPrint("Not fair for them");
debugPrint("they have this: " + theirBadge.cards[theirInd].item + " (" + theirBadge.cards[theirInd].count + ")");
continue; //it's not neutral+, check other options
}
}
debugPrint("it's a match!");
let itemToSend = {
item: myBadge.cards[k].item,
count: 1,
iconUrl: myBadge.cards[k].iconUrl
};
let itemToReceive = {
item: theirBadge.cards[j].item,
count: 1,
iconUrl: theirBadge.cards[j].iconUrl
};
//fill items to send
let sendmatch = itemsToSend.find(item => item.appId == myBadge.appId);
if (sendmatch == undefined) {
let newMatch = {
appId: myBadge.appId,
title: myBadge.title,
cards: [itemToSend]
};
itemsToSend.push(newMatch);
} else {
let existingCard = sendmatch.cards.find((a) => a.item == itemToSend.item);
if (existingCard == undefined) {
sendmatch.cards.push(itemToSend);
} else {
existingCard.count += 1;
}
}
//add this item to their inventory
theirBadge.cards[theirInd].count += 1;
//remove this item from our inventory
myBadge.cards[k].count -= 1;
//fill items to receive
let receiveMatch = itemsToReceive.find((item) => item.appId == myBadge.appId);
if (receiveMatch == undefined) {
let newMatch = {
appId: myBadge.appId,
title: myBadge.title,
cards: [itemToReceive],
};
itemsToReceive.push(newMatch);
} else {
let existingCard = receiveMatch.cards.find((a) => a.item == itemToReceive.item);
if (existingCard == undefined) {
receiveMatch.cards.push(itemToReceive);
} else {
existingCard.count += 1;
}
}
//add this item to our inventory
myBadge.cards[myInd].count += 1;
//remove this item from their inventory
theirBadge.cards[j].count -= 1;
foundMatch = true;
break; //found a match!
}
}
if (foundMatch) { //if we found something - we need to sort cards again and start over.
myBadge.cards.sort((a, b) => b.count - a.count);
myState = calcState(myBadge);
debugPrint("new myState=" + myState);
}
}
}
}
if (!foundMatch) {
break; //found no matches - move to next badge
}
theirBadge.cards.sort((a, b) => b.count - a.count);
}
}
debugPrint("items to send");
debugPrint(JSON.stringify(itemsToSend));
debugPrint("items to receive");
debugPrint(JSON.stringify(itemsToReceive));
bots.Result[index].itemsToSend = itemsToSend;
bots.Result[index].itemsToReceive = itemsToReceive;
if (itemsToSend.length > 0) {
addMatchRow(index);
callback();
} else {
debugPrint("no matches");
callback();
}
}
function GetCards(index, userindex) {
debugPrint("GetCards " + index + " : " + userindex);
if (userindex >= bots.Result.length) {
debugPrint("finished");
debugPrint(new Date(Date.now()));
hideThrobber();
hideMessage();
updateProgress(bots.Result.length - 1);
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
}
if (userindex >=0 &&
(((bots.Result[userindex].MatchEverything == 1) && !globalSettings.anyBots) ||
((bots.Result[userindex].MatchEverything == 0) && !globalSettings.fairBots) ||
(bots.Result[userindex].TotalInventoryCount < globalSettings.botMinItems) ||
((globalSettings.botMaxItems > 0) && (bots.Result[userindex].TotalInventoryCount > globalSettings.botMaxItems)) ||
blacklist.includes(bots.Result[userindex].SteamID))) {
debugPrint("Ignoring bot " + bots.Result[userindex].SteamID);
debugPrint(((bots.Result[userindex].MatchEverything == 1) && !globalSettings.anyBots));
debugPrint(((bots.Result[userindex].MatchEverything == 0) && !globalSettings.fairBots));
debugPrint((bots.Result[userindex].TotalInventoryCount >= globalSettings.botMinItems));
debugPrint(((globalSettings.botMaxItems > 0) && (bots.Result[userindex].TotalInventoryCount <= globalSettings.botMaxItems)));
debugPrint(blacklist.includes(bots.Result[userindex].SteamID));
GetCards(0, userindex+1);
return;
}
if (index == 0) {
botBadges.length = 0;
botBadges = deepClone(myBadges);
for (let i = 0; i < botBadges.length; i++) {
botBadges[i].cards.length = 0;
}
}
if (index < botBadges.length) {
let profileLink;
if (userindex == -1) {
profileLink = myProfileLink;
updateMessage("Getting our data for badge " + (index + 1) + " of " + botBadges.length);
} else {
profileLink = "profiles/" + bots.Result[userindex].SteamID;
updateMessage("Fetching bot " + (userindex + 1).toString() + " of " + bots.Result.length.toString() + " (badge " + (index + 1) + " of " + botBadges.length + ")");
updateProgress(userindex);
}
let url = "https://steamcommunity.com/" + profileLink + "/gamecards/" + botBadges[index].appId;
let xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "document";
// eslint-disable-next-line
xhr.onload = function () {
if (stop) {
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
}
let status = xhr.status;
if (status === 200) {
debugPrint("processing badge " + botBadges[index].appId);
if (null !== xhr.response.documentElement.querySelector("body.private_profile")) {
debugPrint("bot has private profile:" + bots.Result[userindex].SteamID);
setTimeout(
(function (index, userindex) {
return function () {
GetCards(index, userindex);
};
})(0, userindex+1),
globalSettings.weblimiter + globalSettings.errorLimiter * errors
);
return;
}
let badgeCards = xhr.response.documentElement.querySelectorAll(".badge_card_set_card");
if (badgeCards.length >= 5) {
errors = 0;
botBadges[index].maxCards = badgeCards.length;
for (let i = 0; i < badgeCards.length; i++) {
let quantityElement = badgeCards[i].querySelector(".badge_card_set_text_qty");
let quantity = quantityElement == null ? "(0)" : quantityElement.innerText.trim();
quantity = quantity.slice(1, -1);
let name = "";
badgeCards[i].querySelector(".badge_card_set_title").childNodes.forEach(function (element) {
if (element.nodeType === Node.TEXT_NODE) {
name = name + element.textContent;
}
});
name = name.trim();
let icon = badgeCards[i].querySelector(".gamecard").src.trim();
let newcard = {
item: name,
count: Number(quantity),
iconUrl: icon,
};
debugPrint(JSON.stringify(newcard));
botBadges[index].cards.push(newcard);
}
//Check for same hash but different name, ask user to report
if (userindex == -1) {
for (let j = 0; j < botBadges[index].cards.length; j++) {
for (let i = 0; i < botBadges[index].cards.length; i++) {
if (i != j && botBadges[index].cards[i].name != botBadges[index].cards[j].name &&
botBadges[index].cards[j].iconUrl.substring(botBadges[index].cards[j].iconUrl.length - 5) == botBadges[index].cards[i].iconUrl.substring(botBadges[index].cards[i].iconUrl.length - 5)) {
unsafeWindow.ShowAlertDialog('WARNING', 'Different cards have same signature, please report this: ' + botBadges[index].appId);
}
}
}
}
index++;
setTimeout(
(function (index, userindex) {
return function () {
GetCards(index, userindex);
};
})(index, userindex),
globalSettings.weblimiter
);
return;
} else {
//if can't find any cards on badge page - retry, that's must be a bug.
debugPrint(xhr.response.documentElement.outerHTML);
errors++;
}
} else {
errors++;
}
if ((status < 400 || status >= 500) && errors <= globalSettings.maxErrors) {
setTimeout(
(function (index, userindex) {
return function () {
GetCards(index, userindex);
};
})(index, userindex),
globalSettings.weblimiter + globalSettings.errorLimiter * errors
);
} else {
if (status != 200) {
updateMessage("Error getting badge data, ERROR " + status);
} else {
updateMessage("Error getting badge data, malformed HTML");
}
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
}
};
// eslint-disable-next-line
xhr.onerror = function () {
if (stop) {
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
}
errors++;
if (errors <= globalSettings.maxErrors) {
setTimeout(
(function (index, userindex) {
return function () {
GetCards(index, userindex);
};
})(index, userindex),
globalSettings.weblimiter + globalSettings.errorLimiter * errors
);
return;
} else {
debugPrint("error");
updateMessage("Error getting badge data");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
}
};
xhr.send();
return; //do this synchronously to avoid rate limit
}
debugPrint("populated");
debugTime("Filter and sort");
for (let i = botBadges.length - 1; i >= 0; i--) {
debugPrint("badge " + i + JSON.stringify(botBadges[i]));
botBadges[i].cards.sort((a, b) => b.count - a.count);
if (userindex < 0) {
if (botBadges[i].cards[0].count - botBadges[i].cards[botBadges[i].cards.length - 1].count < 2) {
//nothing to match, remove from list.
botBadges.splice(i, 1);
continue;
}
}
let totalCards = 0;
for (let j = 0; j < botBadges[i].maxCards; j++) {
totalCards += botBadges[i].cards[j].count;
}
botBadges[i].maxSets = Math.floor(totalCards / botBadges[i].maxCards);
botBadges[i].lastSet = Math.ceil(totalCards / botBadges[i].maxCards);
debugPrint("totalCards=" + totalCards + " maxSets=" + botBadges[i].maxSets + " lastSet=" + botBadges[i].lastSet);
}
debugTimeEnd("Filter and sort");
if (userindex < 0) {
if (botBadges.length == 0) {
hideThrobber();
updateMessage("No cards to match");
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
} else {
myBadges = deepClone(botBadges);
GetCards(0, 0);
return;
}
} else {
debugPrint(bots.Result[userindex].SteamID);
compareCards(userindex, function () {
setTimeout(
(function (userindex) {
return function () {
GetCards(0, userindex);
};
})(userindex + 1),
globalSettings.weblimiter
);
});
}
}
function getBadges(page) {
let url = "https://steamcommunity.com/" + myProfileLink + "/badges?p=" + page;
let xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "document";
xhr.onload = function () {
if (stop) {
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
}
let status = xhr.status;
if (status === 200) {
errors = 0;
debugPrint("processing page " + page);
updateMessage("Processing badges page " + page);
if (page === 1) {
let pageLinks = xhr.response.documentElement.getElementsByClassName("pagelink");
if (pageLinks.length > 0) {
maxPages = Number(pageLinks[pageLinks.length - 1].textContent.trim());
}
}
let badges = xhr.response.documentElement.getElementsByClassName("badge_row_inner");
for (let i = 0; i < badges.length; i++) {
if (badges[i].getElementsByClassName("owned").length > 0) {
//we only need badges where we have at least one card, and no special badges
if (!badges[i].parentElement.querySelector(".badge_row_overlay").href.endsWith("border=1")) {
//ignore foil badges completely for now. TODO: match foils too.
let appidNodes = badges[i].getElementsByClassName("card_drop_info_dialog");
if (appidNodes.length > 0) {
let appidText = appidNodes[0].getAttribute("id");
let appidSplitted = appidText.split("_");
if (appidSplitted.length >= 5) {
let appId = Number(appidSplitted[4]);
let maxCards = 0;
if (badges[i].getElementsByClassName("badge_craft_button").length === 0) {
let maxCardsText = badges[i].getElementsByClassName("badge_progress_info")[0].innerText.trim();
let maxCardsSplitted = maxCardsText.split(" ");
maxCards = Number(maxCardsSplitted[2]);
}
let title = badges[i].querySelector(".badge_title").childNodes[0].textContent.trim();
let badgeStub = {
appId: appId,
title: title,
maxCards: maxCards,
maxSets: 0,
lastSet: 0,
cards: [],
};
myBadges.push(badgeStub);
}
}
}
}
}
page++;
} else {
errors++;
}
if ((status < 400 || status >= 500) && errors <= globalSettings.maxErrors) {
if (page <= maxPages) {
setTimeout(
(function (page) {
return function () {
getBadges(page);
};
})(page),
globalSettings.weblimiter + globalSettings.errorLimiter * errors
);
} else {
debugPrint("all badge pages processed");
debugPrint(globalSettings.weblimiter + globalSettings.errorLimiter * errors);
if (myBadges.length === 0) {
hideThrobber();
updateMessage("No cards to match");
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
} else {
setTimeout(function () {
GetCards(0, -1);
}, globalSettings.weblimiter + globalSettings.errorLimiter * errors);
}
}
} else {
if (status != 200) {
updateMessage("Error getting badge page, ERROR " + status);
} else {
updateMessage("Error getting badge page, malformed HTML");
}
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
}
};
xhr.onerror = function () {
if (stop) {
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
}
errors++;
if (errors <= globalSettings.maxErrors) {
setTimeout(
(function (page) {
return function () {
getBadges(page);
};
})(page),
globalSettings.weblimiter + globalSettings.errorLimiter * errors
);
} else {
debugPrint("error getting badge page");
updateMessage("Error getting badge page");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.remove();
return;
}
};
xhr.send();
}
function filterEventHandler(event) {
let appId = event.target.id.split("_")[1];
let matches = document.getElementsByClassName("asf_stm_appid_" + appId);
for (let i = 0; i < matches.length; i++) {
matches[i].style.display = event.target.checked ? "inline-block" : "none";
checkRow(matches[i].parentElement.parentElement);
}
}
function filterSwitchesHandler(event) {
let action = event.target.id.split("_")[3];
let filterWidget = document.getElementById("asf_stm_filters_body");
let checkboxes = filterWidget.getElementsByTagName("input");
for (let i = 0; i < checkboxes.length; i++) {
if (action === "all") {
if (!checkboxes[i].checked) {
checkboxes[i].checked = true;
filterEventHandler({ target: checkboxes[i] });
}
} else if (action === "none") {
if (checkboxes[i].checked) {
checkboxes[i].checked = false;
filterEventHandler({ target: checkboxes[i] });
}
} else if (action === "invert") {
checkboxes[i].checked = !checkboxes[i].checked;
filterEventHandler({ target: checkboxes[i] });
}
}
}
function filtersButtonEvent() {
let filterWidget = document.getElementById("asf_stm_filters");
if (filterWidget.style.marginRight == "-50%") {
filterWidget.style.marginRight = "unset";
} else {
filterWidget.style.marginRight = "-50%";
}
}
function stopButtonEvent() {
let stopButton = document.getElementById("asf_stm_stop");
stopButton.removeEventListener("click", stopButtonEvent, false);
stopButton.title = "Stopping...";
stopButton.classList.add("btn_disabled");
updateMessage("Stopping...");
stop = true;
}
function buttonPressedEvent() {
if (bots === null || bots.Result === undefined || bots.Result.length == 0 || bots.Success != true || bots.cacheTime + botCacheTime < Date.now()) {
debugPrint("Bot cache invalidated");
fetchBots();
return;
}
bots.Result.sort(botSorter);
disableButton();
debugPrint(new Date(Date.now()));
let mainContentDiv = document.getElementsByClassName("maincontent")[0];
mainContentDiv.textContent = "";
mainContentDiv.style.width = "90%";
mainContentDiv.innerHTML = `
<div class="profile_badges_header">
<div id="throbber">
<div class="LoadingWrapper">
<div class="LoadingThrobber">
<div class="Bar Bar1"></div>
<div class="Bar Bar2"></div>
<div class="Bar Bar3"></div>
</div>
</div>
</div>
<div>
<div id="asf_stm_messagebox" class="profile_badges_header">
<div id="asf_stm_message" class="profile_badges_header_title" style="text-align: center;">Initialization</div>
</div>
</div>
<div style="width: 100%;">
<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">
<span>🛑</span>
</div>
<div style="width: auto;overflow: hidden;" class="profile_xp_block_remaining_bar">
<div id="asf_stm_progress" class="profile_xp_block_remaining_bar_progress" style="width: 100%;">
</div>
</div>
</div>
</div>
<div id="asf_stm_filters" style="position: fixed; z-index: 1000; right: 5px; bottom: 45px; transition-duration: 500ms;
transition-timing-function: ease; margin-right: -50%; padding: 5px; max-width: 40%; display: inline-block; border-radius: 2px;
background:${globalSettings.filterBackgroundColor}; color: #67c1f5;">
<div style="white-space: nowrap;">Select:
<a id="asf_stm_filter_all" class="commentthread_pagelinks">
all
</a>
<a id="asf_stm_filter_none" class="commentthread_pagelinks">
none
</a>
<a id="asf_stm_filter_invert" class="commentthread_pagelinks">
invert
</a>
</div>
<hr />
<div id="asf_stm_filters_body">
<span id="asf_stm_placeholder" style="margin-right: 15px;">No matches to filter</span>
</div>
</div>
<div style="position: fixed;z-index: 1000;right: 5px;bottom: 5px;" id="asf_stm_filters_button_div">
<a id="asf_stm_filters_button" class="btnv6_blue_hoverfade btn_medium">
<span>Filters</span>
</a>
</div>
`;
document.getElementById("asf_stm_stop").addEventListener("click", stopButtonEvent, false);
document.getElementById("asf_stm_filters_body").addEventListener("change", filterEventHandler);
document.getElementById("asf_stm_filter_all").addEventListener("click", filterSwitchesHandler);
document.getElementById("asf_stm_filter_none").addEventListener("click", filterSwitchesHandler);
document.getElementById("asf_stm_filter_invert").addEventListener("click", filterSwitchesHandler);
document.getElementById("asf_stm_filters_button").addEventListener("click", filtersButtonEvent, false);
maxPages = 1;
stop = false;
myBadges.length = 0;
getBadges(1);
}
function botSorter(a, b) {
let result = 0;
for (let i = 0; i < globalSettings.sortBotsBy.length; i++) {
switch (globalSettings.sortBotsBy[i]) {
case 'MatchEverythingFirst':
result = b.MatchEverything - a.MatchEverything;
break;
case 'MatchEverythingLast':
result = a.MatchEverything - b.MatchEverything;
break;
case 'TotalGamesCountDesc':
result = b.TotalGamesCount - a.TotalGamesCount;
break;
case 'TotalGamesCountAsc':
result = a.TotalGamesCount - b.TotalGamesCount;
break;
case 'TotalItemsCountDesc':
result = b.TotalItemsCount - a.TotalItemsCount;
break;
case 'TotalItemsCountAsc':
result = a.TotalItemsCount - b.TotalItemsCount;
break;
case 'TotalInventoryCountDesc':
result = b.TotalInventoryCount - a.TotalInventoryCount;
break;
case 'TotalInventoryCountAsc':
result = a.TotalInventoryCount - b.TotalInventoryCount;
break;
}
if (result !== 0) {
break;
}
}
return result;
}
function fetchBots() {
let requestUrl = "https://asf.justarchi.net/Api/Listing/Bots";
let requestFunc;
if (typeof GM_xmlhttpRequest !== "function") {
requestFunc = GM.xmlHttpRequest.bind(GM);
} else {
requestFunc = GM_xmlhttpRequest;
}
requestFunc({
method: "GET",
url: requestUrl,
onload: function (response) {
if (response.status != 200) {
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
debugPrint("can't fetch list of bots, ERROR=" + response.status);
debugPrint(JSON.stringify(response));
return;
}
try {
let re = /("SteamID":)(\d+)/g;
let fixedJson = response.response.replace(re, '$1"$2"'); //because fuck js
bots = JSON.parse(fixedJson);
bots.cacheTime = Date.now();
if (bots.Success) {
debugPrint("found total " + bots.Result.length + " bots");
localStorage.setItem("Ryzhehvost.ASF.STM.BotCache", JSON.stringify(bots));
buttonPressedEvent();
} else {
//ASF backend does not indicate success
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots, try later");
debugPrint("can't fetch list of bots");
debugPrint(bots.Message);
debugPrint(JSON.stringify(response));
return;
}
return;
} catch (e) {
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots, try later");
debugPrint("can't fetch list of bots");
debugPrint(e);
debugPrint(JSON.stringify(response));
return;
}
},
onerror: function (response) {
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
debugPrint("can't fetch list of bots");
debugPrint(JSON.stringify(response));
},
onabort: function (response) {
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
debugPrint("can't fetch list of bots - aborted");
debugPrint(JSON.stringify(response));
},
ontimeout: function (response) {
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
debugPrint("can't fetch list of bots - timeout");
debugPrint(JSON.stringify(response));
},
});
}
//Main
LoadConfig();
localStorage.removeItem("Ryzhehvost.ASF.STM") //we used to store classid database here before, clean this up.
if (document.getElementsByClassName("badge_details_set_favorite").length != 0) {
let profileRegex = /http[s]?:\/\/steamcommunity.com\/(.*)\/badges.*/g;
let result = profileRegex.exec(document.location);
if (result) {
myProfileLink = result[1];
} else {
//should never happen, but whatever.
myProfileLink = "my";
}
debugPrint(profileRegex);
let botCache = JSON.parse(localStorage.getItem("Ryzhehvost.ASF.STM.BotCache"));
if (botCache === null || botCache.cacheTime === undefined || botCache.cacheTime === null || botCache.cacheTime + botCacheTime < Date.now()) {
botCache = null;
debugPrint("Bot cache invalidated");
} else {
bots = botCache;
}
let buttonDiv = document.createElement("div");
buttonDiv.setAttribute("class", "profile_small_header_additional");
buttonDiv.setAttribute("style", "margin-top: 40px; right: 70px");
buttonDiv.setAttribute("id", "asf_stm_button_div");
buttonDiv.setAttribute("title", "Scan ASF STM");
let button = document.createElement("a");
button.setAttribute("class", "btnv6_blue_hoverfade btn_medium");
button.setAttribute("id", "asf_stm_button");
button.appendChild(document.createElement("span"));
button.firstChild.appendChild(document.createTextNode("Scan ASF STM"));
buttonDiv.appendChild(button);
let anchor = document.getElementsByClassName("profile_small_header_texture")[0];
anchor.appendChild(buttonDiv);
let confButtonDiv = document.createElement("div");
confButtonDiv.setAttribute("class", "profile_small_header_additional");
confButtonDiv.setAttribute("style", "margin-top: 40px;");
confButtonDiv.setAttribute("id", "asf_stm_config_div");
confButtonDiv.setAttribute("title", "Configuration");
let confButton = document.createElement("a");
confButton.setAttribute("class", "btnv6_blue_hoverfade btn_medium_thin");
confButton.setAttribute("id", "asf_stm_config");
confButton.appendChild(document.createElement("span"));
confButton.firstChild.appendChild(document.createTextNode("⚙️"));
confButtonDiv.appendChild(confButton);
anchor.appendChild(confButtonDiv);
confButton.addEventListener("click", ShowConfigDialog, false);
enableButton();
// add our styles to the document's style sheet
if (typeof GM_addStyle != "undefined") {
GM_addStyle(css);
} else {
const node = document.createElement("style");
node.appendChild(document.createTextNode(css));
const heads = document.getElementsByTagName("head");
if (heads.length > 0) {
heads[0].appendChild(node);
} else {
// no head yet, stick it whereever
document.documentElement.appendChild(node);
}
}
} else {
//All code below is a modified version of SteamTrade Matcher Userscript by Tithen-Firion
//Original can be found on https://github.com/Tithen-Firion/STM-UserScript
// MIT License
// Copyright (c) 2017 Tithen-Firion
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
function getRandomInt(min, max) {
"use strict";
return Math.floor(Math.random() * (max - min)) + min;
}
function mySort(a, b) {
"use strict";
return parseInt(b.id) - parseInt(a.id);
}
///// Steam functions /////
function restoreCookie(oldCookie) {
"use strict";
if (oldCookie) {
let now = new Date();
let time = now.getTime();
time += 15 * 24 * 60 * 60 * 1000;
now.setTime(time);
document.cookie = 'strTradeLastInventoryContext=' + oldCookie + '; expires=' + now.toUTCString() + '; path=/tradeoffer/';
}
}
function addCards(g_s, g_v) {
"use strict";
let tmpCards, inv, index, currentCards;
let failLater = false;
let cardTypes = [[], []];
g_v.Cards.forEach(function (requestedCards, i) {
tmpCards = {};
inv = g_v.Users[i].rgContexts[753][6].inventory;
inv.BuildInventoryDisplayElements();
inv = inv.rgInventory;
Object.keys(inv).forEach(function (item) {
// add all matching cards to temporary dict
index = requestedCards.findIndex(function (elem) {
if (globalSettings.debug) {
if (inv[item].icon_url !== inv[item].icon_url_large) {
let tagindex = inv[item].tags.findIndex( (tag) => tag.category === "item_class");
if ((tagindex > -1) && inv[item].tags[tagindex].internal_name === "item_class_2") {
debugPrint("DIFFERENT ICONS: " + inv[item].name + ":\n" + inv[item].icon_url + "\n" + inv[item].icon_url_large);
}
}
}
return ((elem.appid == inv[item].market_fee_app) && ((elem.name === inv[item].name) || inv[item].icon_url.endsWith(elem.hash)));
});
if (index > -1) {
if (tmpCards[requestedCards[index].id] === undefined) {
tmpCards[requestedCards[index].id] = [];
}
tmpCards[requestedCards[index].id].push({type: inv[item].type, element: inv[item].element, id: inv[item].id});
}
});
if (g_s.order === 'SORT') {
// sort cards descending by card id for each type
Object.keys(tmpCards).forEach(function (id) {
tmpCards[id].sort(mySort);
});
}
// add cards to trade in order given by STM
requestedCards.forEach(function (elem) {
currentCards = tmpCards[elem.id] || []; // all cards from inventory with requested signature
if (currentCards.length === 0) {
failLater = true;
} else {
index = 0;
if (g_s.order === 'RANDOM') {
// randomize index
index = getRandomInt(0, currentCards.length);
}
unsafeWindow.MoveItemToTrade(currentCards[index].element);
cardTypes[i].push(currentCards[index].type);
currentCards.splice(index, 1);
}
});
});
if(failLater || document.querySelectorAll('#your_slots .has_item').length != document.querySelectorAll('#their_slots .has_item').length) {
unsafeWindow.ShowAlertDialog('Items missing', 'Some items are missing and were not added to trade offer. Script aborting.');
throw ('Cards missing');
}
// check if item types match
cardTypes[1].forEach(function (type) {
index = cardTypes[0].indexOf(type);
if (index > -1) {
cardTypes[0].splice(index, 1);
} else {
unsafeWindow.ShowAlertDialog('Not 1:1 trade', 'This is not a valid 1:1 trade. Script aborting.');
throw ('Not 1:1 trade');
}
});
restoreCookie(g_v.oldCookie);
// inject some JS to do something after trade offer is sent
if (g_s.doAfterTrade !== 'NOTHING') {
let functionToInject = 'let doAfterTrade = "' + g_s.doAfterTrade + '";';
functionToInject += '$J(document).ajaxSuccess(function (event, xhr, settings) {';
functionToInject += 'if (settings.url === "https://steamcommunity.com/tradeoffer/new/send") {';
functionToInject += 'if (doAfterTrade === "CLOSE_WINDOW") { window.close();';
functionToInject += '} else if (doAfterTrade === "CLICK_OK") {';
functionToInject += 'document.querySelector("div.newmodal_buttons > div").click(); } } });';
let script = document.createElement('script');
script.appendChild(document.createTextNode(functionToInject));
document.body.appendChild(script);
}
// send trade offer
if (g_s.autoSend) {
unsafeWindow.ToggleReady(true);
unsafeWindow.CTradeOfferStateManager.ConfirmTradeOffer();
}
}
function checkContexts(g_s, g_v) {
"use strict";
let ready = 0;
// check if Steam loaded everything needed
g_v.Users.forEach(function (user) {
if (user.rgContexts && user.rgContexts[753] && user.rgContexts[753][6]) {
if (user.cLoadsInFlight === 0) {
if (user.rgContexts[753][6].inventory) {
ready += 1;
} else {
unsafeWindow.document.getElementById('trade_inventory_unavailable').show();
unsafeWindow.document.getElementById('trade_inventory_pending').show();
user.loadInventory(753, 6);
}
}
}
});
if (ready === 2) {
// select your inventory
unsafeWindow.TradePageSelectInventory(g_v.Users[0], 753, "6");
// set trade offer message
document.getElementById('trade_offer_note').value = g_s.tradeMessage;
try {
addCards(g_s, g_v);
} catch (e) {
// no matter what happens, restore old cookie
restoreCookie(g_v.oldCookie);
debugPrint(e);
}
} else {
window.setTimeout(checkContexts, 500, g_s, g_v);
}
}
function getUrlVars() {
"use strict";
let vars = [];
let hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
hashes.forEach(function (hash) {
hash = hash.split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
});
return vars;
}
///// STM functions /////
function ParseCard(card) {
let decodedVar = decodeURIComponent(card);
const re = new RegExp("(.{5})\.([0-9]+)\-(.*)");
let result = decodedVar.match(re);
if (result && result.length == 4) {
return {
id:card,
appid:result[2],
name: result[3],
hash: result[1]
}
} else {
unsafeWindow.ShowAlertDialog('ASF STM Error', 'Failed to parse vars, please report it!');
throw ('Failed to parse');
}
}
try {
LoadConfig();
// get cards data from URL
let vars = getUrlVars();
let Cards = [
(vars.you
? vars.you.split(';').map(elem => ParseCard(elem))
: []),
(vars.them
? vars.them.split(';').map(elem => ParseCard(elem))
: [])
];
if (Cards[0].length !== Cards[1].length) {
unsafeWindow.ShowAlertDialog(
'Different items amount',
'You\'ve requested ' + (Cards[0].length > Cards[1].length
? 'less'
: 'more') + ' items than you give. Script aborting.'
);
throw ('Different items amount on both sides');
}
// clear cookie containing last opened inventory tab - prevents unwanted inventory loading (it will be restored later)
let oldCookie = document.cookie.split('strTradeLastInventoryContext=')[1];
if (oldCookie) {
oldCookie = oldCookie.split(';')[0];
}
document.cookie = 'strTradeLastInventoryContext=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/tradeoffer/';
let Users = [unsafeWindow.UserYou, unsafeWindow.UserThem];
let global_vars = {"Users": Users, "oldCookie": oldCookie, "Cards": Cards};
window.setTimeout(checkContexts, 500, globalSettings, global_vars);
} catch (e) {
debugPrint(e);
}
}
})();