- // ==UserScript==
- // @name Torn Market Filler
- // @namespace https://github.com/SOLiNARY
- // @version 0.4.3
- // @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.
- // @author Ramin Quluzade, Silmaril [2665762]
- // @license MIT License
- // @match https://www.torn.com/page.php?sid=ItemMarket*
- // @match https://*.torn.com/page.php?sid=ItemMarket*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
- // @run-at document-idle
- // @grant GM_addStyle
- // @grant GM_registerMenuCommand
- // ==/UserScript==
-
- (async function() {
- 'use strict';
-
- try {
- GM_registerMenuCommand('Set Price Delta', setPriceDelta);
- GM_registerMenuCommand('Set Api Key', function() { checkApiKey(false); });
- } catch (error) {
- console.warn('[TornMarketFiller] Tampermonkey not detected!');
- }
-
- const itemUrl = "https://api.torn.com/torn/{itemId}?selections=items&key={apiKey}&comment=MarketFiller";
- const marketUrl = "https://api.torn.com/v2/market/{itemId}?selections=itemMarket&key={apiKey}&comment=MarketFiller";
- const marketUrlV2 = "https://api.torn.com/v2/market?id={itemId}&selections=itemMarket&key={apiKey}&comment=MarketFiller";
- let priceDeltaRaw = localStorage.getItem("silmaril-torn-market-filler-price-delta") ?? localStorage.getItem("silmaril-torn-bazaar-filler-price-delta") ?? '-1[0]';
- let apiKey = localStorage.getItem("silmaril-torn-bazaar-filler-apikey") ?? '###PDA-APIKEY###';
-
- let GM_addStyle = function (s) {
- let style = document.createElement("style");
- style.type = "text/css";
- style.innerHTML = s;
- document.head.appendChild(style);
- };
-
- 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}`);
-
- const pages = { "AddItems": 10, "ViewItems": 20, "Other": 0};
- let currentPage = pages.Other;
- let holdTimer;
- const CLICK_TO_AUTOFILL = 'Click to autofill the price and quantity if not set already';
- const LOADING_THE_PRICES = 'Loading the prices...';
-
- const isMobileView = window.innerWidth <= 784;
-
- const observerTarget = document.querySelector("#item-market-root");
- const observerConfig = { attributes: false, childList: true, characterData: false, subtree: true };
- const observer = new MutationObserver(function(mutations) {
- mutations.forEach(mutationRaw => {
- let mutation = mutationRaw.target;
- currentPage = getCurrentPage();
- if (currentPage == pages.AddItems){
- if (mutation.id.startsWith('headlessui-tabs-panel-')) {
- mutation.querySelectorAll('[class*=itemRowWrapper___]:not(.silmaril-market-filler-processed) > [class*=itemRow___]:not([class*=grayedOut___]) [class^=priceInputWrapper___]').forEach(x => AddFillButton(x));
- }
- if (mutation.className.indexOf('priceInputWrapper___') > -1){
- AddFillButton(mutation);
- }
- } else if (currentPage == pages.ViewItems){
- if (mutation.className.startsWith('viewListingWrapper___')) {
- mutation.querySelectorAll('[class*=itemRowWrapper___]:not(.silmaril-market-filler-processed) > [class*=itemRow___]:not([class*=grayedOut___]) [class^=priceInputWrapper___]').forEach(x => AddFillButton(x));
- }
- }
- });
- });
- observer.observe(observerTarget, observerConfig);
-
- function AddFillButton(itemPriceElement){
- if (itemPriceElement.querySelector('.silmaril-market-filler-button') != null){
- return;
- }
- const wrapperParent = findParentByCondition(itemPriceElement, (el) => el.className.indexOf('itemRowWrapper___') > -1);
- wrapperParent.classList.add('silmaril-market-filler-processed');
- let itemIdString = wrapperParent.querySelector('[class^=itemRow___] [type=button][class^=viewInfoButton___]').getAttribute('aria-controls');
- let itemImage = wrapperParent.querySelector('[class*=viewInfoButton] img');
- let itemId = currentPage == pages.AddItems ? getItemIdFromString(itemIdString) : getItemIdFromImage(itemImage);
- const span = document.createElement('span');
- span.title = CLICK_TO_AUTOFILL;
- span.className = 'silmaril-market-filler-button input-money-symbol';
- span.setAttribute('data-action-flag', 'fill');
- span.addEventListener('click', async () => {await handleFillClick(itemId)});
- span.addEventListener('mousedown', startHold);
- span.addEventListener('touchstart', startHold);
- span.addEventListener('mouseup', cancelHold);
- span.addEventListener('mouseleave', cancelHold);
- span.addEventListener('touchend', cancelHold);
- span.addEventListener('touchcancel', cancelHold);
- const input = document.createElement('input');
- // input.title = 'Click to autofill the price & quantity if not set already';
- input.type = 'button';
- input.className = 'wai-btn';
- span.appendChild(input);
- itemPriceElement.querySelector('.input-money-group').prepend(span);
- }
-
- async function GetPrices(itemId){
- let requestUrl = priceDeltaRaw.indexOf('[market]') != -1 ? itemUrl : marketUrlV2;
- requestUrl = requestUrl
- .replace("{itemId}", itemId)
- .replace("{apiKey}", apiKey);
- return fetch(requestUrl)
- .then(response => response.json())
- .then(data => {
- if (data.error != null){
- switch (data.error.code){
- case 2:
- apiKey = null;
- localStorage.setItem("silmaril-torn-bazaar-filler-apikey", null);
- console.error("[TornMarketFiller] Incorrect Api Key:", data);
- return {"price": 'Wrong API key!', "amount": 0};
- case 9:
- console.warn("[TornMarketFiller] The API is temporarily disabled, please try again later");
- return {"price": 'API is OFF!', "amount": 0};
- default:
- console.error("[TornMarketFiller] Error:", data.error.error);
- return {"price": data.error.error, "amount": 0};
- }
- }
- if (priceDeltaRaw.indexOf('[market]') != -1){
- return {"price": data.items[itemId].market_value, "amount": 1};
- } else {
- if (data.itemmarket.listings[0].price == null){
- console.warn("[TornMarketFiller] The API is temporarily disabled, please try again later");
- return {"price": 'API is OFF!', "amount": 0};
- }
- // temporary hotfix to avoid wrong prices
- if (data.itemmarket.item.id != itemId){
- return {"price": 'API is BROKEN!', "amount": 0};
- }
- return data.itemmarket.listings;
- }
- })
- .catch(error => {
- console.error("[TornMarketFiller] Error fetching data:", error);
- return 'Failed!';
- });
-
- }
-
- function GetPrice(prices){
- if (prices == null){
- return 'No prices loaded';
- }
- if (prices.amount == 0){
- return prices.price;
- }
- if (priceDeltaRaw.indexOf('[market]') != -1) {
- prices = Array(prices);
- let priceDelta = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
- return Math.round(performOperation(prices[0].price, priceDelta));
- } else if (priceDeltaRaw.indexOf('[median]') != -1) {
- let priceDelta = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
- return Math.round(performOperation(getMedianPrice(prices), priceDelta));
- } else {
- let marketSlotOffset = priceDeltaRaw.indexOf('[') == -1 ? 0 : parseInt(priceDeltaRaw.substring(priceDeltaRaw.indexOf('[') + 1, priceDeltaRaw.indexOf(']')));
- let priceDeltaWithoutMarketOffset = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
- return Math.round(performOperation(prices[Math.min(marketSlotOffset, prices.length - 1)].price, priceDeltaWithoutMarketOffset));
- }
- }
-
- async function handleFillClick(itemId){
- let target = event.target;
- let tooltipId = target.getAttribute('aria-describedby');
- let tooltip = document.getElementById(tooltipId).querySelector('.ui-tooltip-content');
- target.title = LOADING_THE_PRICES;
- if (tooltip != null){
- tooltip.innerHTML = tooltip.innerHTML.replace(CLICK_TO_AUTOFILL, LOADING_THE_PRICES);
- }
- let inputEvent = new Event("input", {bubbles: true});
- let action = target.getAttribute('data-action-flag');
- let prices = await GetPrices(itemId);
- const breakdown = GetPricesBreakdown(prices);
- target.title = breakdown;
- if (tooltip != null){
- tooltip.innerHTML = tooltip.innerHTML.replace(LOADING_THE_PRICES, breakdown);
- }
- let price = action == 'fill' ? GetPrice(prices) : '';
-
- switchActionFlag(target);
- let parentRow = findParentByCondition(target, (el) => el.className.indexOf('info___') > -1);
- let quantityInputs = parentRow.querySelectorAll('[class^=amountInputWrapper___] .input-money-group > .input-money');
- if (quantityInputs.length > 0){
- if (quantityInputs[0].value.length == 0 || parseInt(quantityInputs[0].value) < 1){
- quantityInputs[0].value = action == 'fill' ? Number.MAX_SAFE_INTEGER : 0;
- quantityInputs[1].value = action == 'fill' ? Number.MAX_SAFE_INTEGER : 0;
- } else {
- quantityInputs[0].value = action == 'clear' ? '' : quantityInputs[0].value;
- quantityInputs[1].value = action == 'clear' ? '' : quantityInputs[1].value;
- }
- quantityInputs[0].dispatchEvent(inputEvent);
- } else {
- let checkbox = parentRow.querySelector('[class^=checkboxWrapper___] > [class^=checkboxContainer___] [type=checkbox]');
- if (action == 'fill' && !checkbox.checked || action == 'clear' && checkbox.checked){
- checkbox.click();
- }
- }
-
- let priceInputs = target.parentNode.querySelectorAll('input.input-money');
- priceInputs.forEach(x => {x.value = price});
- priceInputs[0].dispatchEvent(inputEvent);
- }
-
- function getItemIdFromString(string){
- const match = string.match(/-(\d+)-/);
- if (match) {
- const number = match[1];
- return number;
- } else {
- console.error("[TornMarketFiller] ItemId not found!");
- return -1;
- }
- }
-
- function getItemIdFromImage(image){
- let numberPattern = /\/(\d+)\//;
- let match = image.src.match(numberPattern);
- if (match) {
- return parseInt(match[1], 10);
- } else {
- console.error("[TornMarketFiller] ItemId not found!");
- return -1;
- }
- }
-
- function switchActionFlag(target){
- switch (target.getAttribute('data-action-flag')){
- case 'fill':
- target.setAttribute('data-action-flag', 'clear');
- break;
- case 'clear':
- default:
- target.setAttribute('data-action-flag', 'fill');
- break;
- }
- }
-
- function findParentByCondition(element, conditionFn){
- let currentElement = element;
- while (currentElement !== null) {
- if (conditionFn(currentElement)) {
- return currentElement;
- }
- currentElement = currentElement.parentElement;
- }
- return null;
- }
-
- function setPriceDelta() {
- let userInput = prompt('Enter price delta formula (default: -1[0]):', priceDeltaRaw);
- if (userInput !== null) {
- priceDeltaRaw = userInput;
- localStorage.setItem("silmaril-torn-market-filler-price-delta", userInput);
- } else {
- console.error("[TornMarketFiller] User cancelled the Price Delta input.");
- }
- }
-
- function GetPricesBreakdown(prices){
- if (prices[0] == undefined){
- prices = Array(prices);
- }
- const sb = new StringBuilder();
- for (let i = 0; i < Math.min(prices.length, 5); i++){
- sb.append(`${prices[i].amount} x ${formatNumberWithCommas(prices[i].price)}`);
- if (i < Math.min(prices.length+1, 5)){
- sb.append('<br>');
- }
- }
- return sb.toString();
- }
-
- function performOperation(number, operation) {
- // Parse the operation string to extract the operator and value
- const match = operation.match(/^([-+]?)(\d+(?:\.\d+)?)(%)?$/);
-
- if (!match) {
- throw new Error('Invalid operation string');
- }
-
- const [, operator, operand, isPercentage] = match;
- const operandValue = parseFloat(operand);
-
- // Check for percentage and convert if necessary
- const adjustedOperand = isPercentage ? (number * operandValue) / 100 : operandValue;
-
- // Perform the operation based on the operator
- switch (operator) {
- case '':
- case '+':
- return number + adjustedOperand;
- case '-':
- return number - adjustedOperand;
- default:
- throw new Error('Invalid operator');
- }
- }
-
- function formatNumberWithCommas(number) {
- return new Intl.NumberFormat('en-US').format(number);
- }
-
- function checkApiKey(checkExisting = true) {
- if (!checkExisting || apiKey === null || apiKey.indexOf('PDA-APIKEY') > -1 || apiKey.length != 16){
- let userInput = prompt("Please enter a PUBLIC Api Key, it will be used to get current bazaar prices:", apiKey ?? '');
- if (userInput !== null && userInput.length == 16) {
- apiKey = userInput;
- localStorage.setItem("silmaril-torn-bazaar-filler-apikey", userInput);
- } else {
- console.error("[TornMarketFiller] User cancelled the Api Key input.");
- }
- }
- }
-
- function getMedianPrice(items) {
- // Expand prices according to their quantities
- const prices = items.flatMap(item => Array(item.amount).fill(item.price));
-
- // Sort prices in ascending order
- prices.sort((a, b) => a - b);
-
- // Find the median
- const mid = Math.floor(prices.length / 2);
-
- if (prices.length % 2 === 0) {
- // If even number of prices, take the average of the two middle prices
- return (prices[mid - 1] + prices[mid]) / 2;
- } else {
- // If odd, take the middle price
- return prices[mid];
- }
- }
-
- function getCurrentPage(){
- if (window.location.href.indexOf('#/addListing') > -1){
- return pages.AddItems;
- } else if (window.location.href.indexOf('#/viewListing') > -1){
- return pages.ViewItems;
- } else {
- return pages.Other;
- }
- }
-
- const startHold = () => {
- holdTimer = setTimeout(() => {
- setPriceDelta();
- checkApiKey(false);
- }, 2000);
- };
-
- const cancelHold = () => {
- clearTimeout(holdTimer);
- };
-
- class StringBuilder {
- constructor() {
- this.parts = [];
- }
-
- append(str) {
- this.parts.push(str);
- return this;
- }
-
- toString() {
- return this.parts.join('');
- }
- }
- })();