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 })