// ==UserScript==
// @name Steam Economy Enhancer
// @icon https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg
// @namespace https://github.com/Nuklon
// @author Nuklon
// @license MIT
// @version 6.9.2
// @description Enhances the Steam Inventory and Steam Market.
// @include *://steamcommunity.com/id/*/inventory*
// @include *://steamcommunity.com/profiles/*/inventory*
// @include *://steamcommunity.com/market*
// @include *://steamcommunity.com/tradeoffer*
// @require https://code.jquery.com/jquery-3.3.1.min.js
// @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
// @require https://raw.githubusercontent.com/kapetan/jquery-observe/ca67b735bb3ae8d678d1843384ebbe7c02466c61/jquery-observe.js
// @require https://cdnjs.cloudflare.com/ajax/libs/paginationjs/2.1.2/pagination.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/async/2.6.0/async.js
// @require https://cdnjs.cloudflare.com/ajax/libs/localforage/1.7.1/localforage.min.js
// @require https://moment.github.io/luxon/global/luxon.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/list.js/1.5.0/list.js
// @require https://raw.githubusercontent.com/rmariuzzo/checkboxes.js/91bec667e9172ceb063df1ecb7505e8ed0bae9ba/src/jquery.checkboxes.js
// @grant unsafeWindow
// @homepageURL https://github.com/Nuklon/Steam-Economy-Enhancer
// @supportURL https://github.com/Nuklon/Steam-Economy-Enhancer/issues
// @downloadURL https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js
// @updateURL https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js
// ==/UserScript==
// jQuery is already added by Steam, force no conflict mode.
(function($, async) {
$.noConflict(true);
var DateTime = luxon.DateTime;
const STEAM_INVENTORY_ID = 753;
const PAGE_MARKET = 0;
const PAGE_MARKET_LISTING = 1;
const PAGE_TRADEOFFER = 2;
const PAGE_INVENTORY = 3;
const COLOR_ERROR = '#8A4243';
const COLOR_SUCCESS = '#407736';
const COLOR_PENDING = '#908F44';
const COLOR_PRICE_FAIR = '#496424';
const COLOR_PRICE_CHEAP = '#837433';
const COLOR_PRICE_EXPENSIVE = '#813030';
const COLOR_PRICE_NOT_CHECKED = '#26566c';
const ERROR_SUCCESS = null;
const ERROR_FAILED = 1;
const ERROR_DATA = 2;
var marketLists = [];
var totalNumberOfProcessedQueueItems = 0;
var totalNumberOfQueuedItems = 0;
var totalPriceWithFeesOnMarket = 0;
var totalPriceWithoutFeesOnMarket = 0;
var totalScrap = 0;
var spinnerBlock =
'
';
var numberOfFailedRequests = 0;
var enableConsoleLog = false;
var country = typeof unsafeWindow.g_strCountryCode !== 'undefined' ? unsafeWindow.g_strCountryCode : undefined;
var isLoggedIn = typeof unsafeWindow.g_rgWalletInfo !== 'undefined' && unsafeWindow.g_rgWalletInfo != null || (typeof unsafeWindow.g_bLoggedIn !== 'undefined' && unsafeWindow.g_bLoggedIn);
var currentPage = window.location.href.includes('.com/market') ?
(window.location.href.includes('market/listings') ?
PAGE_MARKET_LISTING :
PAGE_MARKET) :
(window.location.href.includes('.com/tradeoffer') ?
PAGE_TRADEOFFER :
PAGE_INVENTORY);
var market = new SteamMarket(unsafeWindow.g_rgAppContextData,
typeof unsafeWindow.g_strInventoryLoadURL !== 'undefined' && unsafeWindow.g_strInventoryLoadURL != null
? unsafeWindow.g_strInventoryLoadURL
: typeof unsafeWindow.g_strProfileURL !== 'undefined' && unsafeWindow.g_strProfileURL != null
? unsafeWindow.g_strProfileURL + '/inventory/json/'
: 'https://steamcommunity.com/my/inventory/json/',
isLoggedIn ? unsafeWindow.g_rgWalletInfo : undefined);
var currencyId =
isLoggedIn &&
market != null &&
market.walletInfo != null &&
market.walletInfo.wallet_currency != null ?
market.walletInfo.wallet_currency :
3;
var currencySymbol = unsafeWindow.GetCurrencySymbol(unsafeWindow.GetCurrencyCode(currencyId));
function SteamMarket(appContext, inventoryUrl, walletInfo) {
this.appContext = appContext;
this.inventoryUrl = inventoryUrl;
this.walletInfo = walletInfo;
this.inventoryUrlBase = inventoryUrl.replace('/inventory/json', '');
if (!this.inventoryUrlBase.endsWith('/'))
this.inventoryUrlBase += '/';
}
//#region Settings
const SETTING_MIN_NORMAL_PRICE = 'SETTING_MIN_NORMAL_PRICE';
const SETTING_MAX_NORMAL_PRICE = 'SETTING_MAX_NORMAL_PRICE';
const SETTING_MIN_FOIL_PRICE = 'SETTING_MIN_FOIL_PRICE';
const SETTING_MAX_FOIL_PRICE = 'SETTING_MAX_FOIL_PRICE';
const SETTING_MIN_MISC_PRICE = 'SETTING_MIN_MISC_PRICE';
const SETTING_MAX_MISC_PRICE = 'SETTING_MAX_MISC_PRICE';
const SETTING_PRICE_OFFSET = 'SETTING_PRICE_OFFSET';
const SETTING_PRICE_MIN_CHECK_PRICE = 'SETTING_PRICE_MIN_CHECK_PRICE';
const SETTING_PRICE_ALGORITHM = 'SETTING_PRICE_ALGORITHM';
const SETTING_PRICE_IGNORE_LOWEST_Q = 'SETTING_PRICE_IGNORE_LOWEST_Q';
const SETTING_PRICE_HISTORY_HOURS = 'SETTING_PRICE_HISTORY_HOURS';
const SETTING_INVENTORY_PRICE_LABELS = 'SETTING_INVENTORY_PRICE_LABELS';
const SETTING_TRADEOFFER_PRICE_LABELS = 'SETTING_TRADEOFFER_PRICE_LABELS';
const SETTING_LAST_CACHE = 'SETTING_LAST_CACHE';
const SETTING_RELIST_AUTOMATICALLY = 'SETTING_RELIST_AUTOMATICALLY';
const SETTING_MARKET_PAGE_COUNT = 'SETTING_MARKET_PAGE_COUNT';
const SETTING_INVENTORY_PRICES = 'SETTING_INVENTORY_PRICES';
var settingDefaults = {
SETTING_MIN_NORMAL_PRICE: 0.05,
SETTING_MAX_NORMAL_PRICE: 2.50,
SETTING_MIN_FOIL_PRICE: 0.15,
SETTING_MAX_FOIL_PRICE: 10,
SETTING_MIN_MISC_PRICE: 0.05,
SETTING_MAX_MISC_PRICE: 10,
SETTING_PRICE_OFFSET: 0.00,
SETTING_PRICE_MIN_CHECK_PRICE: 0.00,
SETTING_PRICE_ALGORITHM: 1,
SETTING_PRICE_IGNORE_LOWEST_Q: 1,
SETTING_PRICE_HISTORY_HOURS: 12,
SETTING_INVENTORY_PRICE_LABELS: 1,
SETTING_TRADEOFFER_PRICE_LABELS: 1,
SETTING_LAST_CACHE: 0,
SETTING_RELIST_AUTOMATICALLY: 0,
SETTING_MARKET_PAGE_COUNT: 100
};
function getSettingWithDefault(name) {
return getLocalStorageItem(name) || (name in settingDefaults ? settingDefaults[name] : null);
}
function setSetting(name, value) {
setLocalStorageItem(name, value);
}
//#endregion
//#region Storage
var storagePersistent = localforage.createInstance({
name: 'see_persistent'
});
var storageSession;
var currentUrl = new URL(window.location.href);
var noCache = currentUrl.searchParams.get('no-cache') != null;
// This does not work the same as the 'normal' session storage because opening a new browser session/tab will clear the cache.
// For this reason, a rolling cache is used.
if (getSessionStorageItem('SESSION') == null || noCache) {
var lastCache = getSettingWithDefault(SETTING_LAST_CACHE);
if (lastCache > 5)
lastCache = 0;
setSetting(SETTING_LAST_CACHE, lastCache + 1);
storageSession = localforage.createInstance({
name: 'see_session_' + lastCache
});
storageSession.clear(); // Clear any previous data.
setSessionStorageItem('SESSION', lastCache);
} else {
storageSession = localforage.createInstance({
name: 'see_session_' + getSessionStorageItem('SESSION')
});
}
function getLocalStorageItem(name) {
try {
return localStorage.getItem(name);
} catch (e) {
return null;
}
}
function setLocalStorageItem(name, value) {
try {
localStorage.setItem(name, value);
return true;
} catch (e) {
logConsole('Failed to set local storage item ' + name + ', ' + e + '.')
return false;
}
}
function getSessionStorageItem(name) {
try {
return sessionStorage.getItem(name);
} catch (e) {
return null;
}
}
function setSessionStorageItem(name, value) {
try {
sessionStorage.setItem(name, value);
return true;
} catch (e) {
logConsole('Failed to set session storage item ' + name + ', ' + e + '.')
return false;
}
}
//#endregion
//#region Price helpers
function getPriceInformationFromItem(item) {
var isTradingCard = getIsTradingCard(item);
var isFoilTradingCard = getIsFoilTradingCard(item);
return getPriceInformation(isTradingCard, isFoilTradingCard);
}
function getPriceInformation(isTradingCard, isFoilTradingCard) {
var maxPrice = 0;
var minPrice = 0;
if (!isTradingCard) {
maxPrice = getSettingWithDefault(SETTING_MAX_MISC_PRICE);
minPrice = getSettingWithDefault(SETTING_MIN_MISC_PRICE);
} else {
maxPrice = isFoilTradingCard ?
getSettingWithDefault(SETTING_MAX_FOIL_PRICE) :
getSettingWithDefault(SETTING_MAX_NORMAL_PRICE);
minPrice = isFoilTradingCard ?
getSettingWithDefault(SETTING_MIN_FOIL_PRICE) :
getSettingWithDefault(SETTING_MIN_NORMAL_PRICE);
}
maxPrice = maxPrice * 100.0;
minPrice = minPrice * 100.0;
var maxPriceBeforeFees = market.getPriceBeforeFees(maxPrice);
var minPriceBeforeFees = market.getPriceBeforeFees(minPrice);
return {
maxPrice,
minPrice,
maxPriceBeforeFees,
minPriceBeforeFees
};
}
// Calculates the average history price, before the fee.
function calculateAverageHistoryPriceBeforeFees(history) {
var highest = 0;
var total = 0;
if (history != null) {
// Highest average price in the last xx hours.
var timeAgo = Date.now() - (getSettingWithDefault(SETTING_PRICE_HISTORY_HOURS) * 60 * 60 * 1000);
history.forEach(function(historyItem) {
var d = new Date(historyItem[0]);
if (d.getTime() > timeAgo) {
highest += historyItem[1] * historyItem[2];
total += historyItem[2];
}
});
}
if (total == 0)
return 0;
highest = Math.floor(highest / total);
return market.getPriceBeforeFees(highest);
}
// Calculates the listing price, before the fee.
function calculateListingPriceBeforeFees(histogram) {
if (typeof histogram === 'undefined' ||
histogram == null ||
histogram.lowest_sell_order == null ||
histogram.sell_order_graph == null)
return 0;
var listingPrice = market.getPriceBeforeFees(histogram.lowest_sell_order);
var shouldIgnoreLowestListingOnLowQuantity = getSettingWithDefault(SETTING_PRICE_IGNORE_LOWEST_Q) == 1;
if (shouldIgnoreLowestListingOnLowQuantity && histogram.sell_order_graph.length >= 2) {
var listingPrice2ndLowest = market.getPriceBeforeFees(histogram.sell_order_graph[1][0] * 100);
if (listingPrice2ndLowest > listingPrice) {
var numberOfListingsLowest = histogram.sell_order_graph[0][1];
var numberOfListings2ndLowest = histogram.sell_order_graph[1][1];
var percentageLower = (100 * (numberOfListingsLowest / numberOfListings2ndLowest));
// The percentage should change based on the quantity (for example, 1200 listings vs 5, or 1 vs 25).
if (numberOfListings2ndLowest >= 1000 && percentageLower <= 5) {
listingPrice = listingPrice2ndLowest;
} else if (numberOfListings2ndLowest < 1000 && percentageLower <= 10) {
listingPrice = listingPrice2ndLowest;
} else if (numberOfListings2ndLowest < 100 && percentageLower <= 15) {
listingPrice = listingPrice2ndLowest;
} else if (numberOfListings2ndLowest < 50 && percentageLower <= 20) {
listingPrice = listingPrice2ndLowest;
} else if (numberOfListings2ndLowest < 25 && percentageLower <= 25) {
listingPrice = listingPrice2ndLowest;
} else if (numberOfListings2ndLowest < 10 && percentageLower <= 30) {
listingPrice = listingPrice2ndLowest;
}
}
}
return listingPrice;
}
function calculateBuyOrderPriceBeforeFees(histogram) {
if (typeof histogram === 'undefined')
return 0;
return market.getPriceBeforeFees(histogram.highest_buy_order);
}
// Calculate the sell price based on the history and listings.
// applyOffset specifies whether the price offset should be applied when the listings are used to determine the price.
function calculateSellPriceBeforeFees(history, histogram, applyOffset, minPriceBeforeFees, maxPriceBeforeFees) {
var historyPrice = calculateAverageHistoryPriceBeforeFees(history);
var listingPrice = calculateListingPriceBeforeFees(histogram);
var buyPrice = calculateBuyOrderPriceBeforeFees(histogram);
var shouldUseAverage = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1;
var shouldUseBuyOrder = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 3;
// If the highest average price is lower than the first listing, return the offset + that listing.
// Otherwise, use the highest average price instead.
var calculatedPrice = 0;
if (shouldUseBuyOrder && buyPrice !== -2) {
calculatedPrice = buyPrice;
} else if (historyPrice < listingPrice || !shouldUseAverage) {
calculatedPrice = listingPrice;
} else {
calculatedPrice = historyPrice;
}
var changedToMax = false;
// List for the maximum price if there are no listings yet.
if (calculatedPrice == 0) {
calculatedPrice = maxPriceBeforeFees;
changedToMax = true;
}
// Apply the offset to the calculated price, but only if the price wasn't changed to the max (as otherwise it's impossible to list for this price).
if (!changedToMax && applyOffset) {
calculatedPrice = calculatedPrice + (getSettingWithDefault(SETTING_PRICE_OFFSET) * 100);
}
// Keep our minimum and maximum in mind.
calculatedPrice = clamp(calculatedPrice, minPriceBeforeFees, maxPriceBeforeFees);
// In case there's a buy order higher than the calculated price.
if (typeof histogram !== 'undefined' && histogram != null && histogram.highest_buy_order != null) {
var buyOrderPrice = market.getPriceBeforeFees(histogram.highest_buy_order);
if (buyOrderPrice > calculatedPrice)
calculatedPrice = buyOrderPrice;
}
return calculatedPrice;
}
//#endregion
//#region Integer helpers
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getNumberOfDigits(x) {
return (Math.log10((x ^ (x >> 31)) - (x >> 31)) | 0) + 1;
}
function padLeftZero(str, max) {
str = str.toString();
return str.length < max ? padLeftZero("0" + str, max) : str;
}
function replaceNonNumbers(str) {
return str.replace(/\D/g, '');
}
//#endregion
//#region Steam Market
// Sell an item with a price in cents.
// Price is before fees.
SteamMarket.prototype.sellItem = function(item, price, callback /*err, data*/ ) {
var sessionId = readCookie('sessionid');
var itemId = item.assetid || item.id;
$.ajax({
type: "POST",
url: 'https://steamcommunity.com/market/sellitem/',
data: {
sessionid: sessionId,
appid: item.appid,
contextid: item.contextid,
assetid: itemId,
amount: 1,
price: price
},
success: function(data) {
if (data.success === false && isRetryMessage(data.message)) {
callback(ERROR_FAILED, data);
} else {
callback(ERROR_SUCCESS, data);
}
},
error: function(data) {
return callback(ERROR_FAILED, data);
},
crossDomain: true,
xhrFields: {
withCredentials: true
},
dataType: 'json'
});
};
// Removes an item.
// Item is the unique item id.
SteamMarket.prototype.removeListing = function(item, callback /*err, data*/ ) {
var sessionId = readCookie('sessionid');
$.ajax({
type: "POST",
url: window.location.protocol + '//steamcommunity.com/market/removelisting/' + item,
data: {
sessionid: sessionId
},
success: function(data) {
callback(ERROR_SUCCESS, data);
},
error: function() {
return callback(ERROR_FAILED);
},
crossDomain: true,
xhrFields: {
withCredentials: true
},
dataType: 'json'
});
};
// Get the price history for an item.
//
// PriceHistory is an array of prices in the form [data, price, number sold].
// Example: [["Fri, 19 Jul 2013 01:00:00 +0000",7.30050206184,362]]
// Prices are ordered by oldest to most recent.
// Price is inclusive of fees.
SteamMarket.prototype.getPriceHistory = function(item, cache, callback) {
try {
var market_name = getMarketHashName(item);
if (market_name == null) {
callback(ERROR_FAILED);
return;
}
var appid = item.appid;
if (cache) {
var storage_hash = 'pricehistory_' + appid + '+' + market_name;
storageSession.getItem(storage_hash)
.then(function(value) {
if (value != null)
callback(ERROR_SUCCESS, value, true);
else
market.getCurrentPriceHistory(appid, market_name, callback);
})
.catch(function(error) {
market.getCurrentPriceHistory(appid, market_name, callback);
});
} else
market.getCurrentPriceHistory(appid, market_name, callback);
} catch (e) {
return callback(ERROR_FAILED);
}
};
SteamMarket.prototype.getGooValue = function(item, callback) {
try {
var sessionId = readCookie('sessionid');
$.ajax({
type: "GET",
url: this.inventoryUrlBase + 'ajaxgetgoovalue/',
data: {
sessionid: sessionId,
appid: item.market_fee_app,
assetid: item.assetid,
contextid: item.contextid
},
success: function(data) {
callback(ERROR_SUCCESS, data);
},
error: function(data) {
return callback(ERROR_FAILED, data);
},
crossDomain: true,
xhrFields: {
withCredentials: true
},
dataType: 'json'
});
} catch (e) {
return callback(ERROR_FAILED);
}
//http://steamcommunity.com/auction/ajaxgetgoovalueforitemtype/?appid=582980&item_type=18&border_color=0
// OR
//http://steamcommunity.com/my/ajaxgetgoovalue/?sessionid=xyz&appid=535690&assetid=4830605461&contextid=6
//sessionid=xyz
//appid = 535690
//assetid = 4830605461
//contextid = 6
}
// Grinds the item into gems.
SteamMarket.prototype.grindIntoGoo = function(item, callback) {
try {
var sessionId = readCookie('sessionid');
$.ajax({
type: "POST",
url: this.inventoryUrlBase + 'ajaxgrindintogoo/',
data: {
sessionid: sessionId,
appid: item.market_fee_app,
assetid: item.assetid,
contextid: item.contextid,
goo_value_expected: item.goo_value_expected
},
success: function(data) {
callback(ERROR_SUCCESS, data);
},
error: function(data) {
return callback(ERROR_FAILED, data);
},
crossDomain: true,
xhrFields: {
withCredentials: true
},
dataType: 'json'
});
} catch (e) {
return callback(ERROR_FAILED);
}
//sessionid = xyz
//appid = 535690
//assetid = 4830605461
//contextid = 6
//goo_value_expected = 10
//http://steamcommunity.com/my/ajaxgrindintogoo/
}
// Unpacks the booster pack.
SteamMarket.prototype.unpackBoosterPack = function(item, callback) {
try {
var sessionId = readCookie('sessionid');
$.ajax({
type: "POST",
url: this.inventoryUrlBase + 'ajaxunpackbooster/',
data: {
sessionid: sessionId,
appid: item.market_fee_app,
communityitemid: item.assetid
},
success: function(data) {
callback(ERROR_SUCCESS, data);
},
error: function(data) {
return callback(ERROR_FAILED, data);
},
crossDomain: true,
xhrFields: {
withCredentials: true
},
dataType: 'json'
});
} catch (e) {
return callback(ERROR_FAILED);
}
//sessionid = xyz
//appid = 535690
//communityitemid = 4830605461
//http://steamcommunity.com/my/ajaxunpackbooster/
}
// Get the current price history for an item.
SteamMarket.prototype.getCurrentPriceHistory = function(appid, market_name, callback) {
var url = window.location.protocol +
'//steamcommunity.com/market/pricehistory/?appid=' +
appid +
'&market_hash_name=' +
market_name;
$.get(url,
function(data) {
if (!data || !data.success || !data.prices) {
callback(ERROR_DATA);
return;
}
// Multiply prices so they're in pennies.
for (var i = 0; i < data.prices.length; i++) {
data.prices[i][1] *= 100;
data.prices[i][2] = parseInt(data.prices[i][2]);
}
// Store the price history in the session storage.
var storage_hash = 'pricehistory_' + appid + '+' + market_name;
storageSession.setItem(storage_hash, data.prices);
callback(ERROR_SUCCESS, data.prices, false);
},
'json')
.fail(function(data) {
if (!data || !data.responseJSON) {
return callback(ERROR_FAILED);
}
if (!data.responseJSON.success) {
callback(ERROR_DATA);
return;
}
return callback(ERROR_FAILED);
});
}
// Get the item name id from a market item.
//
// This id never changes so we can store this in the persistent storage.
SteamMarket.prototype.getMarketItemNameId = function(item, callback) {
try {
var market_name = getMarketHashName(item);
if (market_name == null) {
callback(ERROR_FAILED);
return;
}
var appid = item.appid;
var storage_hash = 'itemnameid_' + appid + '+' + market_name;
storagePersistent.getItem(storage_hash)
.then(function(value) {
if (value != null)
callback(ERROR_SUCCESS, value);
else
return market.getCurrentMarketItemNameId(appid, market_name, callback);
})
.catch(function(error) {
return market.getCurrentMarketItemNameId(appid, market_name, callback);
});
} catch (e) {
return callback(ERROR_FAILED);
}
}
// Get the item name id from a market item.
SteamMarket.prototype.getCurrentMarketItemNameId = function(appid, market_name, callback) {
var url = window.location.protocol + '//steamcommunity.com/market/listings/' + appid + '/' + market_name;
$.get(url,
function(page) {
var matches = /Market_LoadOrderSpread\( (\d+) \);/.exec(page);
if (matches == null) {
callback(ERROR_DATA);
return;
}
var item_nameid = matches[1];
// Store the item name id in the persistent storage.
var storage_hash = 'itemnameid_' + appid + '+' + market_name;
storagePersistent.setItem(storage_hash, item_nameid);
callback(ERROR_SUCCESS, item_nameid);
})
.fail(function(e) {
return callback(ERROR_FAILED, e.status);
});
};
// Get the sales listings for this item in the market, with more information.
//
//{
//"success" : 1,
//"sell_order_table" : "Price<\/th> | Quantity<\/th><\/tr> |
---|
0,04\u20ac<\/td> | 311<\/td><\/tr> |
0,05\u20ac<\/td> | 895<\/td><\/tr> |
0,06\u20ac<\/td> | 495<\/td><\/tr> |
0,07\u20ac<\/td> | 174<\/td><\/tr> |
0,08\u20ac<\/td> | 49<\/td><\/tr> |
0,09\u20ac or more<\/td> | 41<\/td><\/tr><\/table>",
//"sell_order_summary" : " |