steam卡牌利润最大化

按照美元区出价, 最大化steam卡牌卖出的利润

  1. // ==UserScript==
  2. // @name steam卡牌利润最大化
  3. // @namespace https://github.com/lzghzr/GreasemonkeyJS
  4. // @version 0.2.26
  5. // @author lzghzr
  6. // @description 按照美元区出价, 最大化steam卡牌卖出的利润
  7. // @supportURL https://github.com/lzghzr/GreasemonkeyJS/issues
  8. // @match http://steamcommunity.com/*/inventory/
  9. // @match https://steamcommunity.com/*/inventory/
  10. // @connect finance.pae.baidu.com
  11. // @license MIT
  12. // @grant GM_addStyle
  13. // @grant GM_xmlhttpRequest
  14. // @run-at document-end
  15. // @noframes
  16. // ==/UserScript==
  17. const W = typeof unsafeWindow === 'undefined' ? window : unsafeWindow;
  18. let gInputUSDCNY;
  19. let gDivLastChecked;
  20. let gInputAddCent;
  21. let gSpanQuickSurplus;
  22. let gSpanQuickError;
  23. const gDivItems = [];
  24. const gQuickSells = [];
  25. addCSS();
  26. addUI();
  27. doLoop();
  28. const elmDivActiveInventoryPage = document.querySelector('#inventories');
  29. const observer = new MutationObserver(mutations => {
  30. mutations.forEach(mutation => {
  31. const rt = mutation.target;
  32. if (rt.classList.contains('inventory_page')) {
  33. const itemHolders = rt.querySelectorAll('.itemHolder');
  34. itemHolders.forEach(itemHolder => {
  35. const rgItem = itemHolder.rgItem;
  36. if (rgItem !== undefined && !gDivItems.includes(rgItem.element) && rgItem.description.marketable === 1) {
  37. gDivItems.push(rgItem.element);
  38. const elmDiv = document.createElement('div');
  39. elmDiv.classList.add('scmpItemCheckbox');
  40. rgItem.element.appendChild(elmDiv);
  41. }
  42. });
  43. }
  44. });
  45. });
  46. observer.observe(elmDivActiveInventoryPage, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
  47. async function addUI() {
  48. const elmDivInventoryPageRight = document.querySelector('.inventory_page_right');
  49. const elmDiv = document.createElement('div');
  50. elmDiv.innerHTML = `
  51. <div class="scmpQuickSell">快速以此价格出售:
  52. <span class="btn_green_white_innerfade" id="scmpQuickSellItem">null</span>
  53. <span>
  54. 加价: $
  55. <input class="filter_search_box" id="scmpAddCent" type="number" value="0.00" step="0.01">
  56. </span>
  57. </div>
  58. <div>
  59. 汇率:
  60. <input class="filter_search_box" id="scmpExch" type="number" value="6.50">
  61. <span class="btn_green_white_innerfade" id="scmpQuickAllItem">快速出售</span>
  62. 剩余:
  63. <span id="scmpQuickSurplus">0</span>
  64. 失败:
  65. <span id="scmpQuickError">0</span>
  66. </div>`;
  67. elmDivInventoryPageRight.appendChild(elmDiv);
  68. const elmSpanQuickSellItem = elmDiv.querySelector('#scmpQuickSellItem');
  69. const elmSpanQuickAllItem = document.querySelector('#scmpQuickAllItem');
  70. gInputAddCent = elmDiv.querySelector('#scmpAddCent');
  71. gSpanQuickSurplus = elmDiv.querySelector('#scmpQuickSurplus');
  72. gSpanQuickError = elmDiv.querySelector('#scmpQuickError');
  73. document.addEventListener('click', async (ev) => {
  74. const evt = ev.target;
  75. if (evt.className === 'inventory_item_link') {
  76. elmSpanQuickSellItem.innerText = 'null';
  77. const rgItem = evt.parentNode.rgItem;
  78. const itemInfo = new ItemInfo(rgItem);
  79. const priceOverview = await getPriceOverview(itemInfo);
  80. if (priceOverview !== 'error')
  81. elmSpanQuickSellItem.innerText = priceOverview.formatPrice;
  82. }
  83. else if (evt.classList.contains('scmpItemCheckbox')) {
  84. const rgItem = evt.parentNode.rgItem;
  85. const select = evt.classList.contains('scmpItemSelect');
  86. const changeClass = (elmDiv) => {
  87. const elmCheckbox = elmDiv.querySelector('.scmpItemCheckbox');
  88. if (elmDiv.parentNode.style.display !== 'none' && !elmCheckbox.classList.contains('scmpItemSuccess')) {
  89. elmCheckbox.classList.remove('scmpItemError');
  90. elmCheckbox.classList.toggle('scmpItemSelect', !select);
  91. }
  92. };
  93. if (gDivLastChecked !== undefined && ev.shiftKey) {
  94. const start = gDivItems.indexOf(gDivLastChecked);
  95. const end = gDivItems.indexOf(rgItem.element);
  96. const someDivItems = gDivItems.slice(Math.min(start, end), Math.max(start, end) + 1);
  97. for (const y of someDivItems)
  98. changeClass(y);
  99. }
  100. else
  101. changeClass(rgItem.element);
  102. gDivLastChecked = rgItem.element;
  103. }
  104. });
  105. elmSpanQuickSellItem.addEventListener('click', (ev) => {
  106. const evt = ev.target;
  107. const elmDivActiveInfo = document.querySelector('.activeInfo');
  108. const rgItem = elmDivActiveInfo.rgItem;
  109. const elmDivitemCheck = rgItem.element.querySelector('.scmpItemCheckbox');
  110. if (!elmDivitemCheck.classList.contains('scmpItemSuccess') && evt.innerText !== 'null') {
  111. const price = W.GetPriceValueAsInt(evt.innerText);
  112. const itemInfo = new ItemInfo(rgItem, price);
  113. quickSellItem(itemInfo);
  114. }
  115. });
  116. elmSpanQuickAllItem.addEventListener('click', () => {
  117. const elmDivItemInfos = document.querySelectorAll('.scmpItemSelect');
  118. elmDivItemInfos.forEach(elmDivItemInfo => {
  119. const rgItem = elmDivItemInfo.parentNode.rgItem;
  120. const itemInfo = new ItemInfo(rgItem);
  121. if (rgItem.description.marketable === 1)
  122. gQuickSells.push(itemInfo);
  123. });
  124. });
  125. gInputAddCent.addEventListener('input', () => {
  126. const activeInfo = document.querySelector('.activeInfo > .inventory_item_link');
  127. activeInfo.click();
  128. });
  129. gInputUSDCNY = elmDiv.querySelector('#scmpExch');
  130. const baiduExch = await XHR({
  131. GM: true,
  132. method: 'GET',
  133. url: 'https://finance.pae.baidu.com/vapi/v1/getquotation?group=huilv_minute&need_reverse_real=1&code=USDCNY',
  134. responseType: 'json',
  135. });
  136. if (baiduExch?.body?.Result !== undefined && baiduExch.response.status === 200) {
  137. baiduExch.body.Result.pankouinfos.list.forEach(list => {
  138. if (list.ename === 'preClose')
  139. gInputUSDCNY.value = list.value;
  140. });
  141. }
  142. }
  143. async function getPriceOverview(itemInfo) {
  144. const priceoverview = await XHR({
  145. method: 'GET',
  146. url: `/market/priceoverview/?country=US&currency=1&appid=${itemInfo.rgItem.description.appid}\
  147. &market_hash_name=${encodeURIComponent(W.GetMarketHashName(itemInfo.rgItem.description))}`,
  148. responseType: 'json'
  149. });
  150. const stop = () => itemInfo.status = 'error';
  151. if (priceoverview !== undefined && priceoverview.response.status === 200
  152. && priceoverview.body.success && priceoverview.body.lowest_price) {
  153. itemInfo.lowestPrice = priceoverview.body.lowest_price.replace('$', '');
  154. return calculatePrice(itemInfo);
  155. }
  156. else {
  157. const marketListings = await XHR({
  158. method: 'GET',
  159. url: `/market/listings/${itemInfo.rgItem.description.appid}\
  160. /${encodeURIComponent(W.GetMarketHashName(itemInfo.rgItem.description))}`
  161. });
  162. if (marketListings === undefined || marketListings.response.status !== 200)
  163. return stop();
  164. const marketLoadOrderSpread = marketListings.body.match(/Market_LoadOrderSpread\( (\d+)/);
  165. if (marketLoadOrderSpread === null)
  166. return stop();
  167. const itemordershistogram = await XHR({
  168. method: 'GET',
  169. url: `/market/itemordershistogram/?country=US&language=english&currency=1&item_nameid=${marketLoadOrderSpread[1]}&two_factor=0`,
  170. responseType: 'json'
  171. });
  172. if (itemordershistogram?.body?.sell_order_graph[0] === undefined || itemordershistogram.response.status !== 200
  173. || itemordershistogram.body.success !== 1)
  174. return stop();
  175. itemInfo.lowestPrice = ' ' + itemordershistogram.body.sell_order_graph[0][0];
  176. return calculatePrice(itemInfo);
  177. }
  178. }
  179. function calculatePrice(itemInfo) {
  180. let price = W.GetPriceValueAsInt(itemInfo.lowestPrice);
  181. const addCent = parseFloat(gInputAddCent.value) * 100;
  182. const exchangeRate = parseFloat(gInputUSDCNY.value);
  183. const publisherFee = itemInfo.rgItem.description.market_fee || W.g_rgWalletInfo.wallet_publisher_fee_percent_default;
  184. const feeInfo = W.CalculateFeeAmount(price, publisherFee);
  185. price = price - feeInfo.fees;
  186. itemInfo.price = Math.floor((price + addCent) * exchangeRate);
  187. itemInfo.formatPrice = W.v_currencyformat(itemInfo.price, W.GetCurrencyCode(W.g_rgWalletInfo.wallet_currency));
  188. return itemInfo;
  189. }
  190. async function quickSellItem(itemInfo) {
  191. itemInfo.status = 'run';
  192. const sellitem = await XHR({
  193. method: 'POST',
  194. url: 'https://steamcommunity.com/market/sellitem/',
  195. data: `sessionid=${W.g_sessionID}&appid=${itemInfo.rgItem.description.appid}\
  196. &contextid=${itemInfo.rgItem.contextid}&assetid=${itemInfo.rgItem.assetid}&amount=1&price=${itemInfo.price}`,
  197. responseType: 'json',
  198. withCredentials: true
  199. });
  200. if (sellitem === undefined || sellitem.response.status !== 200 || !sellitem.body.success)
  201. itemInfo.status = 'error';
  202. else
  203. itemInfo.status = 'success';
  204. }
  205. async function doLoop() {
  206. const itemInfo = gQuickSells.shift();
  207. const loop = () => {
  208. setTimeout(() => {
  209. doLoop();
  210. }, 500);
  211. };
  212. if (itemInfo !== undefined) {
  213. const priceOverview = await getPriceOverview(itemInfo);
  214. if (priceOverview !== 'error') {
  215. await quickSellItem(priceOverview);
  216. doLoop();
  217. }
  218. else
  219. loop();
  220. }
  221. else
  222. loop();
  223. }
  224. function addCSS() {
  225. GM_addStyle(`
  226. .scmpItemSelect {
  227. background: yellow;
  228. }
  229. .scmpItemRun {
  230. background: blue;
  231. }
  232. .scmpItemSuccess {
  233. background: green;
  234. }
  235. .scmpItemError {
  236. background: red;
  237. }
  238. .scmpItemCheckbox {
  239. position: absolute;
  240. z-index: 100;
  241. top: 0;
  242. left: 0;
  243. width: 20px;
  244. height: 20px;
  245. border: 2px solid yellow;
  246. opacity: 0.7;
  247. cursor: default;
  248. }
  249. .scmpItemCheckbox:hover {
  250. opacity: 1;
  251. }
  252. #scmpExch {
  253. width: 3.3em;
  254. -moz-appearance: textfield;
  255. }
  256. #scmpExch::-webkit-inner-spin-button {
  257. -webkit-appearance: none;
  258. }
  259. #scmpAddCent {
  260. width: 3.9em;
  261. }`);
  262. }
  263. function XHR(XHROptions) {
  264. return new Promise(resolve => {
  265. const onerror = (error) => {
  266. console.error(error);
  267. resolve(undefined);
  268. };
  269. if (XHROptions.GM) {
  270. if (XHROptions.method === 'POST') {
  271. if (XHROptions.headers === undefined)
  272. XHROptions.headers = {};
  273. if (XHROptions.headers['Content-Type'] === undefined)
  274. XHROptions.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
  275. }
  276. XHROptions.timeout = 30 * 1000;
  277. XHROptions.onload = res => resolve({ response: res, body: res.response });
  278. XHROptions.onerror = onerror;
  279. XHROptions.ontimeout = onerror;
  280. GM_xmlhttpRequest(XHROptions);
  281. }
  282. else {
  283. const xhr = new XMLHttpRequest();
  284. xhr.open(XHROptions.method, XHROptions.url);
  285. if (XHROptions.method === 'POST' && xhr.getResponseHeader('Content-Type') === null)
  286. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
  287. if (XHROptions.withCredentials)
  288. xhr.withCredentials = true;
  289. if (XHROptions.responseType !== undefined)
  290. xhr.responseType = XHROptions.responseType;
  291. xhr.timeout = 30 * 1000;
  292. xhr.onload = ev => {
  293. const res = ev.target;
  294. resolve({ response: res, body: res.response });
  295. };
  296. xhr.onerror = onerror;
  297. xhr.ontimeout = onerror;
  298. xhr.send(XHROptions.data);
  299. }
  300. });
  301. }
  302. class ItemInfo {
  303. constructor(rgItem, price) {
  304. this.rgItem = rgItem;
  305. if (price !== undefined)
  306. this.price = price;
  307. }
  308. rgItem;
  309. price;
  310. formatPrice;
  311. _status = '';
  312. get status() {
  313. return this._status;
  314. }
  315. set status(valve) {
  316. this._status = valve;
  317. const elmCheckbox = this.rgItem.element.querySelector('.scmpItemCheckbox');
  318. if (elmCheckbox === null)
  319. return;
  320. switch (valve) {
  321. case 'run':
  322. elmCheckbox.classList.remove('scmpItemError');
  323. elmCheckbox.classList.remove('scmpItemSelect');
  324. elmCheckbox.classList.add('scmpItemRun');
  325. break;
  326. case 'success':
  327. gSpanQuickSurplus.innerText = gQuickSells.length.toString();
  328. elmCheckbox.classList.remove('scmpItemError');
  329. elmCheckbox.classList.remove('scmpItemRun');
  330. elmCheckbox.classList.add('scmpItemSuccess');
  331. break;
  332. case 'error':
  333. gSpanQuickSurplus.innerText = gQuickSells.length.toString();
  334. gSpanQuickError.innerText = (parseInt(gSpanQuickError.innerText) + 1).toString();
  335. elmCheckbox.classList.remove('scmpItemRun');
  336. elmCheckbox.classList.add('scmpItemError');
  337. elmCheckbox.classList.add('scmpItemSelect');
  338. break;
  339. default:
  340. break;
  341. }
  342. }
  343. lowestPrice;
  344. }