├── LICENSE
├── README.md
└── code.user.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
Steam Economy Enhancer
2 |
3 | A free userscript to enhance your Steam Inventory, Steam Market and Steam Tradeoffers.
4 |
5 | It adds the following features to the Steam Market:
6 |
7 | * Detect overpriced and underpriced items.
8 | * Select 5/25/all (overpriced) items and remove them at once.
9 | * (Automatically) relist overpriced items.
10 | * Sort and search items by name, price or date.
11 | * Total price for listings, as seller and buyer.
12 |
13 | It adds the following features to the Steam Inventory:
14 |
15 | * Sell all (selected) items or trading cards automatically.
16 | * Select multiple items simultaneously with *Shift* or *Ctrl*.
17 | * Market sell and buy listings added to the item details.
18 | * Quick sell buttons to sell an item without confirmations.
19 | * Shows the lowest listed price for each item.
20 | * Turn selected items into gems.
21 | * Unpack selected booster packs.
22 |
23 | It adds the following features to the Steam Tradeoffers:
24 |
25 | * A summary of all items from both parties that includes total number of items, number of unique items and item count breakdown (how many of each item there are)
26 | * Select all items of the current page.
27 | * Shows the lowest listed price for each inventory item.
28 |
29 | The pricing can be based on the lowest listed price, the price history and your own minimum and maximum prices.
30 | This can be defined in Steam Economy Enhancer's settings, which you can find at the top of the page near the *Install Steam* button.
31 |
32 | > [!NOTE]
33 | > It is free but there is **NO** support. If you want to add functionality, feel free to submit a PR.
34 |
35 | ### Download
36 |
37 | [Install Steam Economy Enhancer](https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js)
38 |
39 | *[Violentmonkey](https://violentmonkey.github.io/) is required to install.*
40 |
41 | ### Screenshots
42 |
43 |
44 | *Market*
45 |
46 | 
47 |
48 |
49 | *Inventory*
50 |
51 | 
52 |
53 |
54 | *Options*
55 |
56 | 
57 |
58 |
59 | *Trade offers*
60 |
61 | 
62 |
63 |
64 | ### License
65 |
66 | [MIT](LICENSE)
67 |
--------------------------------------------------------------------------------
/code.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Steam Economy Enhancer
3 | // @icon data:image/svg+xml,%0A%3Csvg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 267 267"%3E%3Ccircle cx="133.3" cy="133.3" r="133.3" fill="%2326566c"/%3E%3Cpath fill="%23ebebeb" fill-rule="nonzero" d="m50 133 83-83 84 83-84 84-83-84Zm83 62 62-61-62-62v123Z"/%3E%3C/svg%3E
4 | // @namespace https://github.com/Nuklon
5 | // @author Nuklon
6 | // @license MIT
7 | // @version 7.1.12
8 | // @description 增强 Steam 库存和 Steam 市场功能
9 | // @match *://steamcommunity.com/id/*/inventory*
10 | // @match *://steamcommunity.com/profiles/*/inventory*
11 | // @match *://steamcommunity.com/market*
12 | // @match *://steamcommunity.com/tradeoffer*
13 | // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
14 | // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.14.1/jquery-ui.min.js
15 | // @require https://cdnjs.cloudflare.com/ajax/libs/async/3.2.6/async.js
16 | // @require https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js
17 | // @require https://cdnjs.cloudflare.com/ajax/libs/luxon/3.5.0/luxon.min.js
18 | // @require https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.js
19 | // @require https://raw.githubusercontent.com/kapetan/jquery-observe/ca67b735bb3ae8d678d1843384ebbe7c02466c61/jquery-observe.js
20 | // @require https://raw.githubusercontent.com/rmariuzzo/checkboxes.js/91bec667e9172ceb063df1ecb7505e8ed0bae9ba/src/jquery.checkboxes.js
21 | // @grant unsafeWindow
22 | // @homepageURL https://keylol.com/t311996-1-1
23 | // @homepage https://keylol.com/t311996-1-1
24 | // @supportURL https://keylol.com/t311996-1-1
25 | // @downloadURL https://raw.githubusercontent.com/Sneer-Cat/Steam-Economy-Enhancer/master/code.user.js
26 | // @updateURL https://raw.githubusercontent.com/Sneer-Cat/Steam-Economy-Enhancer/master/code.user.js
27 | // ==/UserScript==
28 |
29 | /* disable some eslint rules until the code is cleaned up */
30 | /* global unsafeWindow, luxon, jQuery, async, List, localforage */
31 | /* eslint no-undef: off */
32 |
33 | // jQuery is already added by Steam, force no conflict mode.
34 | (function ($, async) {
35 | $.noConflict(true);
36 |
37 | const PAGE_MARKET = 0;
38 | const PAGE_MARKET_LISTING = 1;
39 | const PAGE_TRADEOFFER = 2;
40 | const PAGE_INVENTORY = 3;
41 |
42 | const COLOR_ERROR = '#8A4243';
43 | const COLOR_SUCCESS = '#407736';
44 | const COLOR_PENDING = '#908F44';
45 | const COLOR_PRICE_FAIR = '#496424';
46 | const COLOR_PRICE_CHEAP = '#837433';
47 | const COLOR_PRICE_EXPENSIVE = '#813030';
48 | const COLOR_PRICE_NOT_CHECKED = '#26566c';
49 |
50 | const ERROR_SUCCESS = null;
51 | const ERROR_FAILED = 1;
52 | const ERROR_DATA = 2;
53 |
54 | const marketLists = [];
55 | let totalNumberOfProcessedQueueItems = 0;
56 | let totalNumberOfQueuedItems = 0;
57 | let totalPriceWithFeesOnMarket = 0;
58 | let totalPriceWithoutFeesOnMarket = 0;
59 | let totalScrap = 0;
60 |
61 | const spinnerBlock =
62 | '
';
63 | let numberOfFailedRequests = 0;
64 |
65 | const enableConsoleLog = false;
66 |
67 | const country = typeof unsafeWindow.g_strCountryCode !== 'undefined' ? unsafeWindow.g_strCountryCode : undefined;
68 | const isLoggedIn = typeof unsafeWindow.g_rgWalletInfo !== 'undefined' && unsafeWindow.g_rgWalletInfo != null || typeof unsafeWindow.g_bLoggedIn !== 'undefined' && unsafeWindow.g_bLoggedIn;
69 |
70 | const currentPage = window.location.href.includes('.com/market')
71 | ? window.location.href.includes('market/listings')
72 | ? PAGE_MARKET_LISTING
73 | : PAGE_MARKET
74 | : window.location.href.includes('.com/tradeoffer')
75 | ? PAGE_TRADEOFFER
76 | : PAGE_INVENTORY;
77 |
78 | const market = new SteamMarket(
79 | unsafeWindow.g_rgAppContextData,
80 | getInventoryUrl(),
81 | isLoggedIn ? unsafeWindow.g_rgWalletInfo : undefined
82 | );
83 |
84 | const currencyId =
85 | isLoggedIn &&
86 | market != null &&
87 | market.walletInfo != null &&
88 | market.walletInfo.wallet_currency != null
89 | ? market.walletInfo.wallet_currency
90 | : 3;
91 |
92 | const currencyCountry =
93 | isLoggedIn &&
94 | market != null &&
95 | market.walletInfo != null &&
96 | market.walletInfo.wallet_country != null
97 | ? market.walletInfo.wallet_country
98 | : 'US';
99 |
100 | const currencyCode = unsafeWindow.GetCurrencyCode(currencyId);
101 |
102 | function SteamMarket(appContext, inventoryUrl, walletInfo) {
103 | this.appContext = appContext;
104 | this.inventoryUrl = inventoryUrl;
105 | this.walletInfo = walletInfo;
106 | this.inventoryUrlBase = inventoryUrl.replace('/inventory/json', '');
107 | if (!this.inventoryUrlBase.endsWith('/')) {
108 | this.inventoryUrlBase += '/';
109 | }
110 | }
111 |
112 | function request(url, options, callback) {
113 | let delayBetweenRequests = 300;
114 | let requestStorageHash = 'see:request:last';
115 |
116 | if (url.startsWith('https://steamcommunity.com/market/')) {
117 | requestStorageHash = `${requestStorageHash}:steamcommunity.com/market`;
118 | delayBetweenRequests = 1000;
119 | }
120 |
121 | const lastRequest = JSON.parse(getLocalStorageItem(requestStorageHash) || JSON.stringify({ time: new Date(0), limited: false }));
122 | const timeSinceLastRequest = Date.now() - new Date(lastRequest.time).getTime();
123 |
124 | delayBetweenRequests = lastRequest.limited ? 2.5 * 60 * 1000 : delayBetweenRequests;
125 |
126 | if (timeSinceLastRequest < delayBetweenRequests) {
127 | setTimeout(() => request(...arguments), delayBetweenRequests - timeSinceLastRequest);
128 | return;
129 | }
130 |
131 | lastRequest.time = new Date();
132 | lastRequest.limited = false;
133 |
134 | setLocalStorageItem(requestStorageHash, JSON.stringify(lastRequest));
135 |
136 | $.ajax({
137 | url: url,
138 | type: options.method,
139 | data: options.data,
140 | success: function (data, statusMessage, xhr) {
141 | if (xhr.status === 429) {
142 | lastRequest.limited = true;
143 | setLocalStorageItem(requestStorageHash, JSON.stringify(lastRequest));
144 | }
145 |
146 | if (xhr.status >= 400) {
147 | const error = new Error('HTTP 错误');
148 | error.statusCode = xhr.status;
149 |
150 | callback(error, data);
151 | } else {
152 | callback(null, data)
153 | }
154 | },
155 | error: (xhr) => {
156 | if (xhr.status === 429) {
157 | lastRequest.limited = true;
158 | setLocalStorageItem(requestStorageHash, JSON.stringify(lastRequest));
159 | }
160 |
161 | const error = new Error('请求失败');
162 | error.statusCode = xhr.status;
163 |
164 | callback(error);
165 | },
166 | dataType: options.responseType
167 | });
168 | };
169 |
170 | function getInventoryUrl() {
171 | if (unsafeWindow.g_strInventoryLoadURL) {
172 | return unsafeWindow.g_strInventoryLoadURL;
173 | }
174 |
175 | let profileUrl = `${window.location.origin}/my/`;
176 |
177 | if (unsafeWindow.g_strProfileURL) {
178 | profileUrl = unsafeWindow.g_strProfileURL;
179 | } else {
180 | const avatar = document.querySelector('#global_actions a.user_avatar');
181 |
182 | if (avatar) {
183 | profileUrl = avatar.href;
184 | }
185 | }
186 |
187 | return `${profileUrl.replace(/\/$/, '')}/inventory/json/`;
188 | }
189 |
190 | //#region Settings
191 | const SETTING_MIN_NORMAL_PRICE = 'SETTING_MIN_NORMAL_PRICE';
192 | const SETTING_MAX_NORMAL_PRICE = 'SETTING_MAX_NORMAL_PRICE';
193 | const SETTING_MIN_FOIL_PRICE = 'SETTING_MIN_FOIL_PRICE';
194 | const SETTING_MAX_FOIL_PRICE = 'SETTING_MAX_FOIL_PRICE';
195 | const SETTING_MIN_MISC_PRICE = 'SETTING_MIN_MISC_PRICE';
196 | const SETTING_MAX_MISC_PRICE = 'SETTING_MAX_MISC_PRICE';
197 | const SETTING_PRICE_OFFSET = 'SETTING_PRICE_OFFSET';
198 | const SETTING_PRICE_MIN_CHECK_PRICE = 'SETTING_PRICE_MIN_CHECK_PRICE';
199 | const SETTING_PRICE_MIN_LIST_PRICE = 'SETTING_PRICE_MIN_LIST_PRICE';
200 | const SETTING_PRICE_ALGORITHM = 'SETTING_PRICE_ALGORITHM';
201 | const SETTING_PRICE_IGNORE_LOWEST_Q = 'SETTING_PRICE_IGNORE_LOWEST_Q';
202 | const SETTING_PRICE_HISTORY_HOURS = 'SETTING_PRICE_HISTORY_HOURS';
203 | const SETTING_INVENTORY_PRICE_LABELS = 'SETTING_INVENTORY_PRICE_LABELS';
204 | const SETTING_TRADEOFFER_PRICE_LABELS = 'SETTING_TRADEOFFER_PRICE_LABELS';
205 | const SETTING_QUICK_SELL_BUTTONS = 'SETTING_QUICK_SELL_BUTTONS';
206 | const SETTING_LAST_CACHE = 'SETTING_LAST_CACHE';
207 | const SETTING_RELIST_AUTOMATICALLY = 'SETTING_RELIST_AUTOMATICALLY';
208 |
209 | const settingDefaults = {
210 | SETTING_MIN_NORMAL_PRICE: 0.05,
211 | SETTING_MAX_NORMAL_PRICE: 2.50,
212 | SETTING_MIN_FOIL_PRICE: 0.15,
213 | SETTING_MAX_FOIL_PRICE: 10,
214 | SETTING_MIN_MISC_PRICE: 0.05,
215 | SETTING_MAX_MISC_PRICE: 10,
216 | SETTING_PRICE_OFFSET: 0.00,
217 | SETTING_PRICE_MIN_CHECK_PRICE: 0.00,
218 | SETTING_PRICE_MIN_LIST_PRICE: 0.03,
219 | SETTING_PRICE_ALGORITHM: 1,
220 | SETTING_PRICE_IGNORE_LOWEST_Q: 1,
221 | SETTING_PRICE_HISTORY_HOURS: 12,
222 | SETTING_INVENTORY_PRICE_LABELS: 1,
223 | SETTING_TRADEOFFER_PRICE_LABELS: 1,
224 | SETTING_QUICK_SELL_BUTTONS: 1,
225 | SETTING_LAST_CACHE: 0,
226 | SETTING_RELIST_AUTOMATICALLY: 0
227 | };
228 |
229 | function getSettingWithDefault(name) {
230 | return getLocalStorageItem(name) || (name in settingDefaults ? settingDefaults[name] : null);
231 | }
232 |
233 | function setSetting(name, value) {
234 | setLocalStorageItem(name, value);
235 | }
236 | //#endregion
237 |
238 | //#region Storage
239 |
240 | const storagePersistent = localforage.createInstance({
241 | name: 'see_persistent'
242 | });
243 |
244 | let storageSession;
245 |
246 | const currentUrl = new URL(window.location.href);
247 | const noCache = currentUrl.searchParams.get('no-cache') != null;
248 |
249 | // This does not work the same as the 'normal' session storage because opening a new browser session/tab will clear the cache.
250 | // For this reason, a rolling cache is used.
251 | if (getSessionStorageItem('SESSION') == null || noCache) {
252 | let lastCache = getSettingWithDefault(SETTING_LAST_CACHE);
253 | if (lastCache > 5) {
254 | lastCache = 0;
255 | }
256 |
257 | setSetting(SETTING_LAST_CACHE, lastCache + 1);
258 |
259 | storageSession = localforage.createInstance({
260 | name: `see_session_${lastCache}`
261 | });
262 |
263 | storageSession.clear(); // Clear any previous data.
264 | setSessionStorageItem('SESSION', lastCache);
265 | } else {
266 | storageSession = localforage.createInstance({
267 | name: `see_session_${getSessionStorageItem('SESSION')}`
268 | });
269 | }
270 |
271 | function getLocalStorageItem(name) {
272 | try {
273 | return localStorage.getItem(name);
274 | } catch (e) {
275 | logConsole(`无法获取 localStorage 内容,名称:${name},原因:${e}。`);
276 | return null;
277 | }
278 | }
279 |
280 | function setLocalStorageItem(name, value) {
281 | try {
282 | localStorage.setItem(name, value);
283 | return true;
284 | } catch (e) {
285 | logConsole(`无法设置 localStorage 内容,名称:${name},原因:${e}。`)
286 | return false;
287 | }
288 | }
289 |
290 | function getSessionStorageItem(name) {
291 | try {
292 | return sessionStorage.getItem(name);
293 | } catch (e) {
294 | logConsole(`无法获取 sessionStorage 内容,名称:${name},原因:${e}。`);
295 | return null;
296 | }
297 | }
298 |
299 | function setSessionStorageItem(name, value) {
300 | try {
301 | sessionStorage.setItem(name, value);
302 | return true;
303 | } catch (e) {
304 | logConsole(`无法设置 sessionStorage 内容,名称:${name},原因:${e}。`)
305 | return false;
306 | }
307 | }
308 | //#endregion
309 |
310 | //#region Price helpers
311 | function formatPrice(valueInCents) {
312 | return unsafeWindow.v_currencyformat(valueInCents, currencyCode, currencyCountry);
313 | }
314 |
315 | function getPriceInformationFromItem(item) {
316 | const isTradingCard = getIsTradingCard(item);
317 | const isFoilTradingCard = getIsFoilTradingCard(item);
318 | return getPriceInformation(isTradingCard, isFoilTradingCard);
319 | }
320 |
321 | function getPriceInformation(isTradingCard, isFoilTradingCard) {
322 | let maxPrice = 0;
323 | let minPrice = 0;
324 |
325 | if (!isTradingCard) {
326 | maxPrice = getSettingWithDefault(SETTING_MAX_MISC_PRICE);
327 | minPrice = getSettingWithDefault(SETTING_MIN_MISC_PRICE);
328 | } else {
329 | maxPrice = isFoilTradingCard
330 | ? getSettingWithDefault(SETTING_MAX_FOIL_PRICE)
331 | : getSettingWithDefault(SETTING_MAX_NORMAL_PRICE);
332 | minPrice = isFoilTradingCard
333 | ? getSettingWithDefault(SETTING_MIN_FOIL_PRICE)
334 | : getSettingWithDefault(SETTING_MIN_NORMAL_PRICE);
335 | }
336 |
337 | maxPrice = maxPrice * 100.0;
338 | minPrice = minPrice * 100.0;
339 |
340 | const maxPriceBeforeFees = market.getPriceBeforeFees(maxPrice);
341 | const minPriceBeforeFees = market.getPriceBeforeFees(minPrice);
342 |
343 | return {
344 | maxPrice,
345 | minPrice,
346 | maxPriceBeforeFees,
347 | minPriceBeforeFees
348 | };
349 | }
350 |
351 | // Calculates the average history price, before the fee.
352 | function calculateAverageHistoryPriceBeforeFees(history) {
353 | let highest = 0;
354 | let total = 0;
355 |
356 | if (history != null) {
357 | // Highest average price in the last xx hours.
358 | const timeAgo = Date.now() - getSettingWithDefault(SETTING_PRICE_HISTORY_HOURS) * 60 * 60 * 1000;
359 |
360 | history.forEach((historyItem) => {
361 | const d = new Date(historyItem[0]);
362 | if (d.getTime() > timeAgo) {
363 | highest += historyItem[1] * historyItem[2];
364 | total += historyItem[2];
365 | }
366 | });
367 | }
368 |
369 | if (total == 0) {
370 | return 0;
371 | }
372 |
373 | highest = Math.floor(highest / total);
374 | return market.getPriceBeforeFees(highest);
375 | }
376 |
377 | // Calculates the listing price, before the fee.
378 | function calculateListingPriceBeforeFees(histogram) {
379 | if (typeof histogram === 'undefined' ||
380 | histogram == null ||
381 | histogram.lowest_sell_order == null ||
382 | histogram.sell_order_graph == null) {
383 | return 0;
384 | }
385 |
386 | let listingPrice = market.getPriceBeforeFees(histogram.lowest_sell_order);
387 |
388 | const shouldIgnoreLowestListingOnLowQuantity = getSettingWithDefault(SETTING_PRICE_IGNORE_LOWEST_Q) == 1;
389 |
390 | if (shouldIgnoreLowestListingOnLowQuantity && histogram.sell_order_graph.length >= 2) {
391 | const listingPrice2ndLowest = market.getPriceBeforeFees(histogram.sell_order_graph[1][0] * 100);
392 |
393 | if (listingPrice2ndLowest > listingPrice) {
394 | const numberOfListingsLowest = histogram.sell_order_graph[0][1];
395 | const numberOfListings2ndLowest = histogram.sell_order_graph[1][1];
396 |
397 | const percentageLower = 100 * (numberOfListingsLowest / numberOfListings2ndLowest);
398 |
399 | // The percentage should change based on the quantity (for example, 1200 listings vs 5, or 1 vs 25).
400 | if (numberOfListings2ndLowest >= 1000 && percentageLower <= 5) {
401 | listingPrice = listingPrice2ndLowest;
402 | } else if (numberOfListings2ndLowest < 1000 && percentageLower <= 10) {
403 | listingPrice = listingPrice2ndLowest;
404 | } else if (numberOfListings2ndLowest < 100 && percentageLower <= 15) {
405 | listingPrice = listingPrice2ndLowest;
406 | } else if (numberOfListings2ndLowest < 50 && percentageLower <= 20) {
407 | listingPrice = listingPrice2ndLowest;
408 | } else if (numberOfListings2ndLowest < 25 && percentageLower <= 25) {
409 | listingPrice = listingPrice2ndLowest;
410 | } else if (numberOfListings2ndLowest < 10 && percentageLower <= 30) {
411 | listingPrice = listingPrice2ndLowest;
412 | }
413 | }
414 | }
415 |
416 | return listingPrice;
417 | }
418 |
419 | function calculateBuyOrderPriceBeforeFees(histogram) {
420 | if (typeof histogram === 'undefined') {
421 | return 0;
422 | }
423 |
424 | return market.getPriceBeforeFees(histogram.highest_buy_order);
425 | }
426 |
427 | // Calculate the sell price based on the history and listings.
428 | // applyOffset specifies whether the price offset should be applied when the listings are used to determine the price.
429 | function calculateSellPriceBeforeFees(history, histogram, applyOffset, minPriceBeforeFees, maxPriceBeforeFees) {
430 | const historyPrice = calculateAverageHistoryPriceBeforeFees(history);
431 | const listingPrice = calculateListingPriceBeforeFees(histogram);
432 | const buyPrice = calculateBuyOrderPriceBeforeFees(histogram);
433 |
434 | const shouldUseAverage = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1;
435 | const shouldUseBuyOrder = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 3;
436 |
437 | // If the highest average price is lower than the first listing, return the offset + that listing.
438 | // Otherwise, use the highest average price instead.
439 | let calculatedPrice = 0;
440 | if (shouldUseBuyOrder && buyPrice !== -2) {
441 | calculatedPrice = buyPrice;
442 | } else if (historyPrice < listingPrice || !shouldUseAverage) {
443 | calculatedPrice = listingPrice;
444 | } else {
445 | calculatedPrice = historyPrice;
446 | }
447 |
448 | let changedToMax = false;
449 | // List for the maximum price if there are no listings yet.
450 | if (calculatedPrice == 0) {
451 | calculatedPrice = maxPriceBeforeFees;
452 | changedToMax = true;
453 | }
454 |
455 |
456 | // 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).
457 | if (!changedToMax && applyOffset) {
458 | calculatedPrice = calculatedPrice + getSettingWithDefault(SETTING_PRICE_OFFSET) * 100;
459 | }
460 |
461 |
462 | // Keep our minimum and maximum in mind.
463 | calculatedPrice = clamp(calculatedPrice, minPriceBeforeFees, maxPriceBeforeFees);
464 |
465 |
466 | // In case there's a buy order higher than the calculated price.
467 | if (typeof histogram !== 'undefined' && histogram != null && histogram.highest_buy_order != null) {
468 | const buyOrderPrice = market.getPriceBeforeFees(histogram.highest_buy_order);
469 | if (buyOrderPrice > calculatedPrice) {
470 | calculatedPrice = buyOrderPrice;
471 | }
472 | }
473 |
474 | return calculatedPrice;
475 | }
476 | //#endregion
477 |
478 | //#region Integer helpers
479 | function getRandomInt(min, max) {
480 | return Math.floor(Math.random() * (max - min + 1)) + min;
481 | }
482 |
483 | function getNumberOfDigits(x) {
484 | return (Math.log10((x ^ x >> 31) - (x >> 31)) | 0) + 1;
485 | }
486 |
487 | function padLeftZero(str, max) {
488 | str = str.toString();
489 | return str.length < max ? padLeftZero(`0${str}`, max) : str;
490 | }
491 |
492 | function replaceNonNumbers(str) {
493 | return str.replace(/\D/g, '');
494 | }
495 | //#endregion
496 |
497 | //#region Steam Market
498 |
499 | // Sell an item with a price in cents.
500 | // Price is before fees.
501 | SteamMarket.prototype.sellItem = function (item, price, callback /*err, data*/) {
502 | const url = `${window.location.origin}/market/sellitem/`;
503 |
504 | const options = {
505 | method: 'POST',
506 | data: {
507 | sessionid: readCookie('sessionid'),
508 | appid: item.appid,
509 | contextid: item.contextid,
510 | assetid: item.assetid || item.id,
511 | amount: item.amount || 1,
512 | price: price
513 | },
514 | responseType: 'json'
515 | };
516 |
517 | request(url, options, callback);
518 | };
519 |
520 | // Removes an item.
521 | // Item is the unique item id.
522 | SteamMarket.prototype.removeListing = function (item, isBuyOrder, callback /*err, data*/) {
523 | const url = isBuyOrder
524 | ? `${window.location.origin}/market/cancelbuyorder/`
525 | : `${window.location.origin}/market/removelisting/${item}`;
526 |
527 | const options = {
528 | method: 'POST',
529 | data: {
530 | sessionid: readCookie('sessionid'),
531 | ...(isBuyOrder ? { buy_orderid: item } : {})
532 | },
533 | responseType: 'json'
534 | };
535 |
536 | request(
537 | url,
538 | options,
539 | (error, data) => {
540 | if (error) {
541 | callback(ERROR_FAILED);
542 | return;
543 | }
544 |
545 | callback(ERROR_SUCCESS, data);
546 | }
547 | );
548 | };
549 |
550 | // Get the price history for an item.
551 | //
552 | // PriceHistory is an array of prices in the form [data, price, number sold].
553 | // Example: [["Fri, 19 Jul 2013 01:00:00 +0000",7.30050206184,362]]
554 | // Prices are ordered by oldest to most recent.
555 | // Price is inclusive of fees.
556 | SteamMarket.prototype.getPriceHistory = function (item, cache, callback) {
557 | const shouldUseAverage = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1;
558 |
559 | if (!shouldUseAverage) {
560 | // The price history is only used by the "average price" calculation
561 | return callback(ERROR_SUCCESS, null, true);
562 | }
563 |
564 | try {
565 | const market_name = getMarketHashName(item);
566 | if (market_name == null) {
567 | callback(ERROR_FAILED);
568 | return;
569 | }
570 |
571 | const appid = item.appid;
572 |
573 | if (cache) {
574 | const storage_hash = `pricehistory_${appid}+${market_name}`;
575 |
576 | storageSession.getItem(storage_hash).
577 | then((value) => {
578 | if (value != null) {
579 | callback(ERROR_SUCCESS, value, true);
580 | } else {
581 | market.getCurrentPriceHistory(appid, market_name, callback);
582 | }
583 | }).
584 | catch(() => {
585 | market.getCurrentPriceHistory(appid, market_name, callback);
586 | });
587 | } else {
588 | market.getCurrentPriceHistory(appid, market_name, callback);
589 | }
590 | } catch {
591 | return callback(ERROR_FAILED);
592 | }
593 | };
594 |
595 | SteamMarket.prototype.getGooValue = function (item, callback) {
596 | try {
597 | let appid = item.market_fee_app;
598 |
599 | for (const action of item.owner_actions) {
600 | if (!action.link || !action.link.startsWith('javascript:GetGooValue')) {
601 | continue;
602 | }
603 |
604 | let item_data = action.link.split(',');
605 | let appid = item_data[2].trim();
606 | let item_type = item_data[3].trim();
607 | let border_color = item_data[4].split(' ')[0].trim();
608 |
609 | const url = `${window.location.origin}/auction/ajaxgetgoovalueforitemtype`;
610 |
611 | const options = {
612 | method: 'GET',
613 | data: {
614 | appid: appid,
615 | item_type: item_type,
616 | border_color: border_color
617 | },
618 | responseType: 'json'
619 | };
620 |
621 | request(
622 | url,
623 | options,
624 | (error, data) => {
625 | if (error) {
626 | callback(ERROR_FAILED, data);
627 | return;
628 | }
629 |
630 | callback(ERROR_SUCCESS, data);
631 | }
632 | );
633 | }
634 | } catch (e) {
635 | return callback(ERROR_FAILED);
636 | }
637 | //http://steamcommunity.com/auction/ajaxgetgoovalueforitemtype/?appid=582980&item_type=18&border_color=0
638 | // OR
639 | //http://steamcommunity.com/my/ajaxgetgoovalue/?sessionid=xyz&appid=535690&assetid=4830605461&contextid=6
640 | //sessionid=xyz
641 | //appid = 535690
642 | //assetid = 4830605461
643 | //contextid = 6
644 | };
645 |
646 |
647 | // Grinds the item into gems.
648 | SteamMarket.prototype.grindIntoGoo = function (item, callback) {
649 | try {
650 | const url = `${this.inventoryUrlBase}ajaxgrindintogoo/`;
651 |
652 | const options = {
653 | method: 'POST',
654 | data: {
655 | sessionid: readCookie('sessionid'),
656 | appid: item.market_fee_app,
657 | assetid: item.assetid,
658 | contextid: item.contextid,
659 | goo_value_expected: item.goo_value_expected
660 | },
661 | responseType: 'json'
662 | };
663 |
664 | request(
665 | url,
666 | options,
667 | (error, data) => {
668 | if (error) {
669 | callback(ERROR_FAILED, data);
670 | return;
671 | }
672 |
673 | callback(ERROR_SUCCESS, data);
674 | }
675 | );
676 | } catch {
677 | return callback(ERROR_FAILED);
678 | }
679 |
680 | //sessionid = xyz
681 | //appid = 535690
682 | //assetid = 4830605461
683 | //contextid = 6
684 | //goo_value_expected = 10
685 | //http://steamcommunity.com/my/ajaxgrindintogoo/
686 | };
687 |
688 |
689 | // Unpacks the booster pack.
690 | SteamMarket.prototype.unpackBoosterPack = function (item, callback) {
691 | try {
692 | const url = `${this.inventoryUrlBase}ajaxunpackbooster/`;
693 |
694 | const options = {
695 | method: 'POST',
696 | data: {
697 | sessionid: readCookie('sessionid'),
698 | appid: item.market_fee_app,
699 | communityitemid: item.assetid
700 | },
701 | responseType: 'json'
702 | };
703 |
704 | request(
705 | url,
706 | options,
707 | (error, data) => {
708 | if (error) {
709 | callback(ERROR_FAILED, data);
710 | return;
711 | }
712 |
713 | callback(ERROR_SUCCESS, data);
714 | }
715 | );
716 | } catch {
717 | return callback(ERROR_FAILED);
718 | }
719 |
720 | //sessionid = xyz
721 | //appid = 535690
722 | //communityitemid = 4830605461
723 | //http://steamcommunity.com/my/ajaxunpackbooster/
724 | };
725 |
726 | // Get the current price history for an item.
727 | SteamMarket.prototype.getCurrentPriceHistory = function (appid, market_name, callback) {
728 | const url = `${window.location.origin}/market/pricehistory/`;
729 |
730 | const options = {
731 | method: 'GET',
732 | data: {
733 | appid: appid,
734 | market_hash_name: market_name
735 | },
736 | responseType: 'json'
737 | };
738 |
739 | request(
740 | url,
741 | options,
742 | (error, data) => {
743 | if (error) {
744 | callback(ERROR_FAILED);
745 | return;
746 | }
747 |
748 | if (data && (!data.success || !data.prices)) {
749 | callback(ERROR_DATA);
750 | return;
751 | }
752 |
753 | // Multiply prices so they're in pennies.
754 | for (let i = 0; i < data.prices.length; i++) {
755 | data.prices[i][1] *= 100;
756 | data.prices[i][2] = parseInt(data.prices[i][2]);
757 | }
758 |
759 | // Store the price history in the session storage.
760 | const storage_hash = `pricehistory_${appid}+${market_name}`;
761 | storageSession.setItem(storage_hash, data.prices);
762 |
763 | callback(ERROR_SUCCESS, data.prices, false);
764 | }
765 | );
766 | };
767 |
768 | // Get the item name id from a market item.
769 | //
770 | // This id never changes so we can store this in the persistent storage.
771 | SteamMarket.prototype.getMarketItemNameId = function (item, callback) {
772 | try {
773 | const market_name = getMarketHashName(item);
774 | if (market_name == null) {
775 | callback(ERROR_FAILED);
776 | return;
777 | }
778 |
779 | const appid = item.appid;
780 | const storage_hash = `itemnameid_${appid}+${market_name}`;
781 |
782 | storagePersistent.getItem(storage_hash).
783 | then((value) => {
784 | if (value != null) {
785 | callback(ERROR_SUCCESS, value);
786 | } else {
787 | return market.getCurrentMarketItemNameId(appid, market_name, callback);
788 | }
789 | }).
790 | catch(() => {
791 | return market.getCurrentMarketItemNameId(appid, market_name, callback);
792 | });
793 | } catch {
794 | return callback(ERROR_FAILED);
795 | }
796 | };
797 |
798 | // Get the item name id from a market item.
799 | SteamMarket.prototype.getCurrentMarketItemNameId = function (appid, market_name, callback) {
800 | const url = `${window.location.origin}/market/listings/${appid}/${escapeURI(market_name)}`;
801 |
802 | const options = { method: 'GET' };
803 |
804 | request(
805 | url,
806 | options,
807 | (error, data) => {
808 | if (error) {
809 | callback(ERROR_FAILED);
810 | return;
811 | }
812 |
813 | const matches = (/Market_LoadOrderSpread\( (\d+) \);/).exec(data || '');
814 | if (matches == null) {
815 | callback(ERROR_DATA);
816 | return;
817 | }
818 |
819 | const item_nameid = matches[1];
820 |
821 | // Store the item name id in the persistent storage.
822 | const storage_hash = `itemnameid_${appid}+${market_name}`;
823 | storagePersistent.setItem(storage_hash, item_nameid);
824 |
825 | callback(ERROR_SUCCESS, item_nameid);
826 | }
827 | );
828 | };
829 |
830 | // Get the sales listings for this item in the market, with more information.
831 | //
832 | //{
833 | //"success" : 1,
834 | //"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>",
835 | //"sell_order_summary" : " |