// ==UserScript==
// @name Torn Market Filler
// @namespace https://github.com/SOLiNARY
// @version 0.4
// @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-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 {border-bottom-left-radius: 0 !important;border-top-left-radius: 0 !important;}#item-market-root [class^=viewListingWrapper___] [class^=priceInputWrapper___] > .input-money-group > .input-money {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) {
return getMedianPrice(prices);
} 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[market]):', priceDeltaRaw);
if (userInput !== null) {
priceDeltaRaw = userInput;
localStorage.setItem("silmaril-torn-bazaar-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('');
}
}
})();