Greasy Fork is available in English.

Torn Market Filler

On "Fill" click autofills market item price with lowest market price currently minus $1 (can be customised), fills max quantity for items, marks checkboxes for guns.

  1. // ==UserScript==
  2. // @name Torn Market Filler
  3. // @namespace https://github.com/SOLiNARY
  4. // @version 0.4.3
  5. // @description On "Fill" click autofills market item price with lowest market price currently minus $1 (can be customised), fills max quantity for items, marks checkboxes for guns.
  6. // @author Ramin Quluzade, Silmaril [2665762]
  7. // @license MIT License
  8. // @match https://www.torn.com/page.php?sid=ItemMarket*
  9. // @match https://*.torn.com/page.php?sid=ItemMarket*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
  11. // @run-at document-idle
  12. // @grant GM_addStyle
  13. // @grant GM_registerMenuCommand
  14. // ==/UserScript==
  15.  
  16. (async function() {
  17. 'use strict';
  18.  
  19. try {
  20. GM_registerMenuCommand('Set Price Delta', setPriceDelta);
  21. GM_registerMenuCommand('Set Api Key', function() { checkApiKey(false); });
  22. } catch (error) {
  23. console.warn('[TornMarketFiller] Tampermonkey not detected!');
  24. }
  25.  
  26. const itemUrl = "https://api.torn.com/torn/{itemId}?selections=items&key={apiKey}&comment=MarketFiller";
  27. const marketUrl = "https://api.torn.com/v2/market/{itemId}?selections=itemMarket&key={apiKey}&comment=MarketFiller";
  28. const marketUrlV2 = "https://api.torn.com/v2/market?id={itemId}&selections=itemMarket&key={apiKey}&comment=MarketFiller";
  29. let priceDeltaRaw = localStorage.getItem("silmaril-torn-market-filler-price-delta") ?? localStorage.getItem("silmaril-torn-bazaar-filler-price-delta") ?? '-1[0]';
  30. let apiKey = localStorage.getItem("silmaril-torn-bazaar-filler-apikey") ?? '###PDA-APIKEY###';
  31.  
  32. let GM_addStyle = function (s) {
  33. let style = document.createElement("style");
  34. style.type = "text/css";
  35. style.innerHTML = s;
  36. document.head.appendChild(style);
  37. };
  38.  
  39. GM_addStyle(`#item-market-root [class^="addListingWrapper___"] [class^="panels___"] [class^="priceInputWrapper___"] > .input-money-group > .input-money{font-size:smaller !important;border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}#item-market-root [class^="viewListingWrapper___"] [class^="priceInputWrapper___"] > .input-money-group > .input-money{font-size:smaller !important;border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}`);
  40.  
  41. const pages = { "AddItems": 10, "ViewItems": 20, "Other": 0};
  42. let currentPage = pages.Other;
  43. let holdTimer;
  44. const CLICK_TO_AUTOFILL = 'Click to autofill the price and quantity if not set already';
  45. const LOADING_THE_PRICES = 'Loading the prices...';
  46.  
  47. const isMobileView = window.innerWidth <= 784;
  48.  
  49. const observerTarget = document.querySelector("#item-market-root");
  50. const observerConfig = { attributes: false, childList: true, characterData: false, subtree: true };
  51. const observer = new MutationObserver(function(mutations) {
  52. mutations.forEach(mutationRaw => {
  53. let mutation = mutationRaw.target;
  54. currentPage = getCurrentPage();
  55. if (currentPage == pages.AddItems){
  56. if (mutation.id.startsWith('headlessui-tabs-panel-')) {
  57. mutation.querySelectorAll('[class*=itemRowWrapper___]:not(.silmaril-market-filler-processed) > [class*=itemRow___]:not([class*=grayedOut___]) [class^=priceInputWrapper___]').forEach(x => AddFillButton(x));
  58. }
  59. if (mutation.className.indexOf('priceInputWrapper___') > -1){
  60. AddFillButton(mutation);
  61. }
  62. } else if (currentPage == pages.ViewItems){
  63. if (mutation.className.startsWith('viewListingWrapper___')) {
  64. mutation.querySelectorAll('[class*=itemRowWrapper___]:not(.silmaril-market-filler-processed) > [class*=itemRow___]:not([class*=grayedOut___]) [class^=priceInputWrapper___]').forEach(x => AddFillButton(x));
  65. }
  66. }
  67. });
  68. });
  69. observer.observe(observerTarget, observerConfig);
  70.  
  71. function AddFillButton(itemPriceElement){
  72. if (itemPriceElement.querySelector('.silmaril-market-filler-button') != null){
  73. return;
  74. }
  75. const wrapperParent = findParentByCondition(itemPriceElement, (el) => el.className.indexOf('itemRowWrapper___') > -1);
  76. wrapperParent.classList.add('silmaril-market-filler-processed');
  77. let itemIdString = wrapperParent.querySelector('[class^=itemRow___] [type=button][class^=viewInfoButton___]').getAttribute('aria-controls');
  78. let itemImage = wrapperParent.querySelector('[class*=viewInfoButton] img');
  79. let itemId = currentPage == pages.AddItems ? getItemIdFromString(itemIdString) : getItemIdFromImage(itemImage);
  80. const span = document.createElement('span');
  81. span.title = CLICK_TO_AUTOFILL;
  82. span.className = 'silmaril-market-filler-button input-money-symbol';
  83. span.setAttribute('data-action-flag', 'fill');
  84. span.addEventListener('click', async () => {await handleFillClick(itemId)});
  85. span.addEventListener('mousedown', startHold);
  86. span.addEventListener('touchstart', startHold);
  87. span.addEventListener('mouseup', cancelHold);
  88. span.addEventListener('mouseleave', cancelHold);
  89. span.addEventListener('touchend', cancelHold);
  90. span.addEventListener('touchcancel', cancelHold);
  91. const input = document.createElement('input');
  92. // input.title = 'Click to autofill the price & quantity if not set already';
  93. input.type = 'button';
  94. input.className = 'wai-btn';
  95. span.appendChild(input);
  96. itemPriceElement.querySelector('.input-money-group').prepend(span);
  97. }
  98.  
  99. async function GetPrices(itemId){
  100. let requestUrl = priceDeltaRaw.indexOf('[market]') != -1 ? itemUrl : marketUrlV2;
  101. requestUrl = requestUrl
  102. .replace("{itemId}", itemId)
  103. .replace("{apiKey}", apiKey);
  104. return fetch(requestUrl)
  105. .then(response => response.json())
  106. .then(data => {
  107. if (data.error != null){
  108. switch (data.error.code){
  109. case 2:
  110. apiKey = null;
  111. localStorage.setItem("silmaril-torn-bazaar-filler-apikey", null);
  112. console.error("[TornMarketFiller] Incorrect Api Key:", data);
  113. return {"price": 'Wrong API key!', "amount": 0};
  114. case 9:
  115. console.warn("[TornMarketFiller] The API is temporarily disabled, please try again later");
  116. return {"price": 'API is OFF!', "amount": 0};
  117. default:
  118. console.error("[TornMarketFiller] Error:", data.error.error);
  119. return {"price": data.error.error, "amount": 0};
  120. }
  121. }
  122. if (priceDeltaRaw.indexOf('[market]') != -1){
  123. return {"price": data.items[itemId].market_value, "amount": 1};
  124. } else {
  125. if (data.itemmarket.listings[0].price == null){
  126. console.warn("[TornMarketFiller] The API is temporarily disabled, please try again later");
  127. return {"price": 'API is OFF!', "amount": 0};
  128. }
  129. // temporary hotfix to avoid wrong prices
  130. if (data.itemmarket.item.id != itemId){
  131. return {"price": 'API is BROKEN!', "amount": 0};
  132. }
  133. return data.itemmarket.listings;
  134. }
  135. })
  136. .catch(error => {
  137. console.error("[TornMarketFiller] Error fetching data:", error);
  138. return 'Failed!';
  139. });
  140.  
  141. }
  142.  
  143. function GetPrice(prices){
  144. if (prices == null){
  145. return 'No prices loaded';
  146. }
  147. if (prices.amount == 0){
  148. return prices.price;
  149. }
  150. if (priceDeltaRaw.indexOf('[market]') != -1) {
  151. prices = Array(prices);
  152. let priceDelta = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
  153. return Math.round(performOperation(prices[0].price, priceDelta));
  154. } else if (priceDeltaRaw.indexOf('[median]') != -1) {
  155. let priceDelta = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
  156. return Math.round(performOperation(getMedianPrice(prices), priceDelta));
  157. } else {
  158. let marketSlotOffset = priceDeltaRaw.indexOf('[') == -1 ? 0 : parseInt(priceDeltaRaw.substring(priceDeltaRaw.indexOf('[') + 1, priceDeltaRaw.indexOf(']')));
  159. let priceDeltaWithoutMarketOffset = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
  160. return Math.round(performOperation(prices[Math.min(marketSlotOffset, prices.length - 1)].price, priceDeltaWithoutMarketOffset));
  161. }
  162. }
  163.  
  164. async function handleFillClick(itemId){
  165. let target = event.target;
  166. let tooltipId = target.getAttribute('aria-describedby');
  167. let tooltip = document.getElementById(tooltipId).querySelector('.ui-tooltip-content');
  168. target.title = LOADING_THE_PRICES;
  169. if (tooltip != null){
  170. tooltip.innerHTML = tooltip.innerHTML.replace(CLICK_TO_AUTOFILL, LOADING_THE_PRICES);
  171. }
  172. let inputEvent = new Event("input", {bubbles: true});
  173. let action = target.getAttribute('data-action-flag');
  174. let prices = await GetPrices(itemId);
  175. const breakdown = GetPricesBreakdown(prices);
  176. target.title = breakdown;
  177. if (tooltip != null){
  178. tooltip.innerHTML = tooltip.innerHTML.replace(LOADING_THE_PRICES, breakdown);
  179. }
  180. let price = action == 'fill' ? GetPrice(prices) : '';
  181.  
  182. switchActionFlag(target);
  183. let parentRow = findParentByCondition(target, (el) => el.className.indexOf('info___') > -1);
  184. let quantityInputs = parentRow.querySelectorAll('[class^=amountInputWrapper___] .input-money-group > .input-money');
  185. if (quantityInputs.length > 0){
  186. if (quantityInputs[0].value.length == 0 || parseInt(quantityInputs[0].value) < 1){
  187. quantityInputs[0].value = action == 'fill' ? Number.MAX_SAFE_INTEGER : 0;
  188. quantityInputs[1].value = action == 'fill' ? Number.MAX_SAFE_INTEGER : 0;
  189. } else {
  190. quantityInputs[0].value = action == 'clear' ? '' : quantityInputs[0].value;
  191. quantityInputs[1].value = action == 'clear' ? '' : quantityInputs[1].value;
  192. }
  193. quantityInputs[0].dispatchEvent(inputEvent);
  194. } else {
  195. let checkbox = parentRow.querySelector('[class^=checkboxWrapper___] > [class^=checkboxContainer___] [type=checkbox]');
  196. if (action == 'fill' && !checkbox.checked || action == 'clear' && checkbox.checked){
  197. checkbox.click();
  198. }
  199. }
  200.  
  201. let priceInputs = target.parentNode.querySelectorAll('input.input-money');
  202. priceInputs.forEach(x => {x.value = price});
  203. priceInputs[0].dispatchEvent(inputEvent);
  204. }
  205.  
  206. function getItemIdFromString(string){
  207. const match = string.match(/-(\d+)-/);
  208. if (match) {
  209. const number = match[1];
  210. return number;
  211. } else {
  212. console.error("[TornMarketFiller] ItemId not found!");
  213. return -1;
  214. }
  215. }
  216.  
  217. function getItemIdFromImage(image){
  218. let numberPattern = /\/(\d+)\//;
  219. let match = image.src.match(numberPattern);
  220. if (match) {
  221. return parseInt(match[1], 10);
  222. } else {
  223. console.error("[TornMarketFiller] ItemId not found!");
  224. return -1;
  225. }
  226. }
  227.  
  228. function switchActionFlag(target){
  229. switch (target.getAttribute('data-action-flag')){
  230. case 'fill':
  231. target.setAttribute('data-action-flag', 'clear');
  232. break;
  233. case 'clear':
  234. default:
  235. target.setAttribute('data-action-flag', 'fill');
  236. break;
  237. }
  238. }
  239.  
  240. function findParentByCondition(element, conditionFn){
  241. let currentElement = element;
  242. while (currentElement !== null) {
  243. if (conditionFn(currentElement)) {
  244. return currentElement;
  245. }
  246. currentElement = currentElement.parentElement;
  247. }
  248. return null;
  249. }
  250.  
  251. function setPriceDelta() {
  252. let userInput = prompt('Enter price delta formula (default: -1[0]):', priceDeltaRaw);
  253. if (userInput !== null) {
  254. priceDeltaRaw = userInput;
  255. localStorage.setItem("silmaril-torn-market-filler-price-delta", userInput);
  256. } else {
  257. console.error("[TornMarketFiller] User cancelled the Price Delta input.");
  258. }
  259. }
  260.  
  261. function GetPricesBreakdown(prices){
  262. if (prices[0] == undefined){
  263. prices = Array(prices);
  264. }
  265. const sb = new StringBuilder();
  266. for (let i = 0; i < Math.min(prices.length, 5); i++){
  267. sb.append(`${prices[i].amount} x ${formatNumberWithCommas(prices[i].price)}`);
  268. if (i < Math.min(prices.length+1, 5)){
  269. sb.append('<br>');
  270. }
  271. }
  272. return sb.toString();
  273. }
  274.  
  275. function performOperation(number, operation) {
  276. // Parse the operation string to extract the operator and value
  277. const match = operation.match(/^([-+]?)(\d+(?:\.\d+)?)(%)?$/);
  278.  
  279. if (!match) {
  280. throw new Error('Invalid operation string');
  281. }
  282.  
  283. const [, operator, operand, isPercentage] = match;
  284. const operandValue = parseFloat(operand);
  285.  
  286. // Check for percentage and convert if necessary
  287. const adjustedOperand = isPercentage ? (number * operandValue) / 100 : operandValue;
  288.  
  289. // Perform the operation based on the operator
  290. switch (operator) {
  291. case '':
  292. case '+':
  293. return number + adjustedOperand;
  294. case '-':
  295. return number - adjustedOperand;
  296. default:
  297. throw new Error('Invalid operator');
  298. }
  299. }
  300.  
  301. function formatNumberWithCommas(number) {
  302. return new Intl.NumberFormat('en-US').format(number);
  303. }
  304.  
  305. function checkApiKey(checkExisting = true) {
  306. if (!checkExisting || apiKey === null || apiKey.indexOf('PDA-APIKEY') > -1 || apiKey.length != 16){
  307. let userInput = prompt("Please enter a PUBLIC Api Key, it will be used to get current bazaar prices:", apiKey ?? '');
  308. if (userInput !== null && userInput.length == 16) {
  309. apiKey = userInput;
  310. localStorage.setItem("silmaril-torn-bazaar-filler-apikey", userInput);
  311. } else {
  312. console.error("[TornMarketFiller] User cancelled the Api Key input.");
  313. }
  314. }
  315. }
  316.  
  317. function getMedianPrice(items) {
  318. // Expand prices according to their quantities
  319. const prices = items.flatMap(item => Array(item.amount).fill(item.price));
  320.  
  321. // Sort prices in ascending order
  322. prices.sort((a, b) => a - b);
  323.  
  324. // Find the median
  325. const mid = Math.floor(prices.length / 2);
  326.  
  327. if (prices.length % 2 === 0) {
  328. // If even number of prices, take the average of the two middle prices
  329. return (prices[mid - 1] + prices[mid]) / 2;
  330. } else {
  331. // If odd, take the middle price
  332. return prices[mid];
  333. }
  334. }
  335.  
  336. function getCurrentPage(){
  337. if (window.location.href.indexOf('#/addListing') > -1){
  338. return pages.AddItems;
  339. } else if (window.location.href.indexOf('#/viewListing') > -1){
  340. return pages.ViewItems;
  341. } else {
  342. return pages.Other;
  343. }
  344. }
  345.  
  346. const startHold = () => {
  347. holdTimer = setTimeout(() => {
  348. setPriceDelta();
  349. checkApiKey(false);
  350. }, 2000);
  351. };
  352.  
  353. const cancelHold = () => {
  354. clearTimeout(holdTimer);
  355. };
  356.  
  357. class StringBuilder {
  358. constructor() {
  359. this.parts = [];
  360. }
  361.  
  362. append(str) {
  363. this.parts.push(str);
  364. return this;
  365. }
  366.  
  367. toString() {
  368. return this.parts.join('');
  369. }
  370. }
  371. })();