NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Auto Bazaar Pricing Tool // @namespace https://www.torn.com/profiles.php?XID=2357191 // @version 1.0.4 // @description Automatically price sellable items in bazaar. // @author Pickle_Slinger [2357191] // @license MIT // @include http://www.torn.com/bazaar.php* // @include https://www.torn.com/bazaar.php* // @match *.torn.com/bazaar.php* // @grant none // ==/UserScript== 'use strict'; // Keys & defaults for script. const storage = 'auto-pricer', defaults = { key: null, interval: 1000, setPrices: true, setQuantities: false, preserveCollection: false, collectionCount: 500 }; // Auto-pricer object. let pricer = { // Use stored configuration settings or use defaults. options: JSON.parse(localStorage.getItem(storage)) || defaults, currentTab: null, // Current items. items: {}, /** * Full page loader. */ loader: { elements: { image: null, message: null }, /** * Create loader element and add to page. */ build: function() { const wrapper = $('<div class="loader-wrap"></div>'), loader = $('<img src="https://mir-s3-cdn-cf.behance.net/project_modules/disp/09b24e31234507.564a1d23c07b4.gif" />'), message = $('<div class="loader-message"></div>'); // Style loader image. loader.css({ 'position': 'fixed', 'top': 0, 'left': 0, 'height': '100%', 'width': '100%', 'background': 'rgba(255, 255, 255, .5)', 'object-fit': 'none', 'z-index': 2147483646 }); // Style message text. message.css({ 'position': 'fixed', 'top': '50%', 'left': '50%', 'transform': 'translate(-50%, -50%)', 'z-index': 2147483647 }); // Add loader to document. $('body') .append(wrapper); // Add loader elements to wrapper and hide. wrapper.append(loader) .append(message) .hide(); // Set elements in loader object. this.elements.image = wrapper; this.elements.message = message; return this; }, /** * Update loader message. * @param text {string} | Message to display. */ update: function(text) { this.elements.message.text(text); return this; }, /** * Show loader. * @returns {pricer} */ show: function() { const { image, message } = this.elements; image.show(); message.show(); return this; }, /** * Hide loader. * @returns {pricer} */ hide: function() { const { image, message } = this.elements; image.hide(); message.hide(); return this; } }, /** * Button to trigger scrape start. */ buttons: { elements: { start: null, configure: null }, /** * Create element and add to page. */ build: function() { const buttons = [ $('<a class="linkTitle___2Z2zS auto-pricer-start">Start Auto Pricer</a>'), $('<a class="linkContainer___47uQr inRow___J1Bmd greyLineV___HQIEI auto-pricer-configure">Configure</a>'), $('<a class="linkContainer___47uQr inRow___J1Bmd greyLineV___HQIEI auto-pricer-configure">Report Bugs</a>') ]; $('.linksContainer___2Kgsm') .prepend(buttons); this.elements = { start: buttons[0], configure: buttons[1], report: buttons[2] }; this.setupListeners(); }, /** * Set up button event listener. */ setupListeners: function() { const { start, configure, report } = this.elements; start.on('click', function() { pricer.gatherItems(); }); configure.on('click', function() { pricer.popup.show(); }); report.on('click', function() { var win = window.open('https://www.torn.com/forums.php#/p=threads&f=67&t=16179209&b=0&a=0', '_blank'); win.focus(); }); } }, /** * Configuration popup. */ popup: { elements: { popup: null, background: null }, /** * All configuration option inputs. */ inputs: { key: $('<label>Torn API key <input name="key" type="text" placeholder="Please enter an API key"></label>'), interval: $('<label>API Interval <input name="interval" type="number"></label>'), setPrices: $('<label>Automatically price items? <input name="pricing" type="checkbox"></label>'), setQuantities: $('<label>Automatically set item quantity? <input name="quantity" type="checkbox"></label>'), preserveCollection: $('<label>Do not sell large quantity items? <input name="preserveCollection" type="checkbox"></label>'), collectionCount: $('<label>Ignore quantities of more than <input name="collectionCount" type="number"></label>') }, /** * Create element and add to page. * @returns {pricer} */ build: function() { const popup = $('<div class="settings-popup"></div>'), background = $('<div class="settings-popup-background"></div>'); // Style popup. popup.css({ 'display': 'none', 'position': 'fixed', 'title': 'Auto Bazaar Pricer', 'top': '50%', 'left': '50%', 'transform': 'translate(calc(-50% + 0.5px), calc(-50% + 0.5px))', 'width': 'calc(100% - 20px)', 'max-width': '325px', 'padding': '10px', 'background': '#fff', 'border-radius': '3px', 'box-sizing': 'border-box', 'z-index': 2147483641 }); // Style popup background. background.css({ 'display': 'none', 'position': 'fixed', 'top': 0, 'left': 0, 'height': '100%', 'width': '100%', 'background': 'rgba(0, 0, 0, .5)', 'z-index': 2147483640 }); for (let input in this.inputs) { const currentInput = this.inputs[input], inputElement = currentInput.find('input'), inputType = inputElement.attr('type'), isInput = inputType === 'text' || inputType === 'number'; // Add input to popup. popup.append(currentInput); // Set existing value. isInput ? inputElement.val(this.getOption(input)) : inputElement.prop('checked', this.getOption(input)); // Set up listener for local storage options. this.setupInputListener(input, currentInput); // Style input wrappers. currentInput.css({ 'display': 'flex', 'flex-direction': 'column', 'margin-bottom': '10px', 'font-weight': 'bold' }); // Style inputs. inputElement.css({ 'padding': '5px', 'margin-top': '3px', 'border': '1px solid #ccc', 'border-radius': '3px' }); } // Add popup & background to document. $('body') .append(popup) .append(background); // Set elements in elements object. this.elements.popup = popup; this.elements.background = background; // Set up dismiss popup listeners. this.setupDismissListener(); return this; }, /** * Completely overwrite old configuration options when an option is updated. * @param inputKey {string} | Storage key for configuration option. * @param value {string|boolean} | Value for current input. */ setConfig: function(inputKey, value) { const { key, interval, setPrices, setQuantities, preserveCollection, collectionCount } = pricer.options; // Create new configuration object. let newConfig = { key: key, interval: interval, setPrices: setPrices, setQuantities: setQuantities, preserveCollection: preserveCollection, collectionCount: collectionCount }; // Assign new changed value to new configuration object. newConfig[inputKey] = value; // Update current configuration. pricer.options = newConfig; // Update local storage configuration. localStorage.setItem(storage, JSON.stringify(newConfig)); }, /** * Get configuration option. * @param option {string} | Configuration option stored in configuration object. * @returns {*} */ getOption: function(option) { return pricer.options[option]; }, /** * Set up dismiss listeners for popup. */ setupDismissListener: function() { const self = this; this.elements.background.on('click', function() { self.hide(); }); }, /** * Set up listeners for storing config options in local storage. * @param inputKey {string} | Storage key for configuration option. * @param input {object} | Input element. */ setupInputListener: function(inputKey, input) { const self = this, inputElement = input.find('input'); inputElement.on('change', function() { const currentInput = $(this), inputType = currentInput.attr('type'), isInput = inputType === 'text' || inputType === 'number'; // Change saving behaviour based on whether the input is text/number or checkbox/radio. isInput ? self.setConfig(inputKey, currentInput.val()) : self.setConfig(inputKey, currentInput.prop('checked')); }); }, /** * Show popup. */ show: function() { this.elements.popup.show(); this.elements.background.show(); }, /** * Hide popup. */ hide: function() { this.elements.popup.hide(); this.elements.background.hide(); } }, /** * Update current tab. */ getCurrentTab: function() { const currentTab = $('.ui-tabs-nav') .find('.ui-state-active'), currentTabName = currentTab.find('a') .attr('href') .replace('#', ''); pricer.currentTab = currentTabName !== 'All' ? currentTabName : null; }, /** * Grab all user items from API. */ gatherItems: function() { const self = this, { currentTab } = self; // Show configuration popup when there's no API key. if (!self.options.key) { self.popup.show(); return false; } $.ajax({ url: 'https://api.torn.com/user', data: { selections: 'inventory', key: self.options.key, }, /** * Show loader and update text before AJAX fires. */ beforeSend: function() { self.loader.show() .update('Preparing to gather all user items.'); }, /** * Set up item in items object when scraped. * @param data {object} | Torn API response. */ success: function(data) { const { inventory } = data; // Loop over all items in players inventory. inventory.forEach(function(value) { const { name, ID, type, quantity, market_price, equipped } = value, isMarketable = !!market_price, isEquipped = equipped, isInCurrentTab = currentTab ? type === currentTab : true; // Only add item if it's tradeable. if (isMarketable && isInCurrentTab) { if ((self.options.preserveCollection) && (self.options.collectionCount < quantity)) {} else { if (isEquipped == 0) { self.items[ID] = { name: name, quantity: quantity } } } } }); }, /** * Gather prices every selected interval (to not get API banned). */ complete: function() { let i = 0; // If there are no items, stop script. if ($.isEmptyObject(self.items)) { self.loader.hide(); console.log('No items were scraped. Please try again.'); } self.loader.update('All items gathered.'); for (let id in pricer.items) { setTimeout(function() { self.getPrice(pricer.items[id].name, id); }, pricer.options.interval * i); i++; } }, /** * If anything went wrong, hide the loader. */ error: function() { self.loader.hide(); console.log('There was an error. Please try again.'); } }); }, /** * Get cheapest possible price of a given item. * @param name {string} | Item name. * @param id {number} | Item ID. */ getPrice: function(name, id) { const self = this; $.ajax({ url: 'https://api.torn.com/market/' + id, data: { selections: 'bazaar,itemmarket', key: self.options.key }, /** * Update loader message with current item being scraped. */ beforeSend: function() { self.loader.update('Scraping ' + name + '.'); }, /** * Add listing price to items object. * @param data {object} | Torn API response. */ success: function(data) { const { bazaar, itemmarket } = data, lowestPrices = [bazaar[0].cost, itemmarket[0].cost], cheapest = Math.min(...lowestPrices); // Set price to sell as a dollar lower. self.items[id].price = cheapest - 1; }, /** * When all pricing is finished, hide the loader and add final prices to inputs. */ complete: function() { if (self.isFinished()) { self.loader.hide(); self.applyPricesAndQuantities(); } } }); }, /** * Grab price inputs for inventory items. * @param name {string} | Item name. * @param id {number} | Item ID. */ getInputs: function (name, id) { const self = this, item = $('.items-cont li:visible:not(.disabled)'); // Rather than using IDs we now have to use item names due to the image canvas update which removed // the ability to grab IDs from the URLs. item.each(function () { const currentItem = $(this), itemName = currentItem.find('.name-wrap .t-overflow').text(); // Add inputs to item object. if (name === itemName) { self.items[id].inputs = { price: currentItem.find('input[type="text"].input-money'), quantity: currentItem.find('.amount input') }; } }); }, /** * Check whether item scraping has finished. * @returns {boolean} */ isFinished: function() { const items = this.items, lastItem = items[Object.keys(items)[Object.keys(items) .length - 1]]; return !!lastItem.price; }, /** * Apply prices to price fields. */ applyPricesAndQuantities: function () { for (let item in this.items) { this.getInputs(this.items[item].name, item); const {price, quantity, inputs} = this.items[item], {setPrices, setQuantities} = this.options; // If prices are set to be automatically added. if (setPrices) { inputs.price.val(price); inputs.price.trigger('keyup'); } // If quantities are set to be automatically added. if (setQuantities) { inputs.quantity.val(quantity); if (inputs.quantity.attr('type') === 'checkbox') { inputs.quantity.next('a').click(); } // Cannot trigger this event with jquery for some reason? // Has to be done in vanilla JS. const event = new Event('input', { bubbles: true, cancelable: true, }); // Trigger update event. inputs.quantity[0].dispatchEvent(event); } } } }; // Update current tab. $(document) .on('click', '.ui-tabs-nav li', function() { const { getCurrentTab } = pricer; setTimeout(function() { pricer.items = {}; getCurrentTab(); }, 100); }); // Run script. const isAddPage = window.location.hash === '#/p=add' || window.location.hash === '#/add', { loader, popup, buttons, getCurrentTab } = pricer; window.loadCount = 0; const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.classList && node.classList.contains('input-money')) { if (window.loadCount == 0) { console.log('its working'); // Create all auto pricer elements & update current tab. getCurrentTab(); loader.build(); popup.build(); buttons.build(); window.loadCount = 1; observer.disconnect(); } } } } }) const wrapper = document.querySelector('#bazaarroot') observer.observe(wrapper, { subtree: true, childList: true })