You've already forked tf2wikipricing
refactor: replace prices.tf with pricedb.io
This commit is contained in:
@@ -27,55 +27,35 @@ chrome.runtime.onMessage.addListener(
|
||||
}
|
||||
);
|
||||
|
||||
chrome.runtime.onMessage.addListener(
|
||||
function(request, sender, sendResponse) {
|
||||
if (request.contentScriptQuery == "getPricesTFToken") {
|
||||
fetch('https://api2.prices.tf/auth/access', {
|
||||
method: 'post',
|
||||
headers: new Headers({
|
||||
'Accept': 'application/json'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => sendResponse(json['accessToken']))
|
||||
.catch(error => {
|
||||
console.error("Failed to get access token", error);
|
||||
})
|
||||
return true;
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
class PricesResponse {
|
||||
keys: number
|
||||
metal: number
|
||||
}
|
||||
|
||||
async function priceUsingPricesTF(token: string, sku: string, retries: number = 3): Promise<PricesResponse> {
|
||||
const url = `https://api2.prices.tf/prices/${encodeURIComponent(sku)}`;
|
||||
async function priceUsingPricedb(sku: string, retries: number = 3): Promise<PricesResponse> {
|
||||
const url = `https://pricedb.io/api/item/${encodeURIComponent(sku)}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
})
|
||||
if (response.status === 404 && sku.includes(';') && !sku.includes(';uncraftable')) {
|
||||
const quality: number = parseInt(sku.split(';')[1], 10);
|
||||
if(quality === 6) {
|
||||
// Try uncraftable variant if unique weapon
|
||||
return priceUsingPricesTF(token, sku + ';uncraftable');
|
||||
return priceUsingPricedb(sku + ';uncraftable');
|
||||
}
|
||||
}
|
||||
if(response.status === 503) {
|
||||
// Happens if we send too many requests in a short period of time
|
||||
if(response.status === 429) {
|
||||
// Happens if we send too many requests (rate limit: 180 req/min)
|
||||
// Retry after a few seconds
|
||||
if(retries >= 0) {
|
||||
console.log(`Cloudflare rate limit exceeded, trying again after 1 second, ${retries} retries left`)
|
||||
console.log(`Rate limit exceeded, trying again after 1 second, ${retries} retries left`)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return priceUsingPricesTF(token, sku, retries - 1);
|
||||
return priceUsingPricedb(sku, retries - 1);
|
||||
} else {
|
||||
throw new Error(`Cloudflare rate limit exceeded, stopping`)
|
||||
throw new Error(`Rate limit exceeded, stopping`)
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
@@ -83,8 +63,8 @@ async function priceUsingPricesTF(token: string, sku: string, retries: number =
|
||||
}
|
||||
const data = await response.json();
|
||||
const prices = new PricesResponse();
|
||||
prices.keys = data['sellKeys']
|
||||
prices.metal = data['sellHalfScrap'] / 18.0;
|
||||
prices.keys = data.sell.keys
|
||||
prices.metal = data.sell.metal
|
||||
return prices;
|
||||
}
|
||||
|
||||
@@ -92,24 +72,14 @@ chrome.runtime.onMessage.addListener(
|
||||
function(request, sender, sendResponse) {
|
||||
if (request.contentScriptQuery == "priceSKU") {
|
||||
const sku: string = request.sku
|
||||
const service: string = request.service
|
||||
const token: string = request.token
|
||||
if(token === "" || !token) {
|
||||
sendResponse(new Error("No token provided"))
|
||||
return false;
|
||||
}
|
||||
switch (service) {
|
||||
case "prices.tf": {
|
||||
priceUsingPricesTF(token, sku)
|
||||
priceUsingPricedb(sku)
|
||||
.then((response) => sendResponse(response))
|
||||
.catch(error => {
|
||||
console.error(`Received "${error}" error while pricing ${sku} using prices.tf`)
|
||||
console.error(`Received "${error}" error while pricing ${sku} using pricedb.io`)
|
||||
sendResponse(null);
|
||||
return false;
|
||||
})
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -2,7 +2,6 @@ declare const __ENV_WEBEXTENSION: boolean;
|
||||
declare const __ENV_USERSCRIPT: boolean;
|
||||
|
||||
import { logDebug, log, logError } from './utils/log'
|
||||
import { getPricesToken } from './pricing/pricestf'
|
||||
import itemQualities from 'tf2-static-schema/static/qualities.json';
|
||||
import { getItemIndexByName, getTradableStatusByDefindex, ItemSchema, ItemSlot, prepareSchema, wipeSchema } from './schemaService'
|
||||
import { $T, extractLocaleFromURL } from './utils/localization'
|
||||
@@ -16,7 +15,7 @@ let exchangeRates: ExchangeRates | null;
|
||||
|
||||
let locale: string = 'en'
|
||||
|
||||
/** Exclude these from the pricelist. */
|
||||
/** Exclude these from pricelist. */
|
||||
const excludedQualities = new Set([
|
||||
15, // Decorated
|
||||
5, // Unusual
|
||||
@@ -175,22 +174,11 @@ async function inject() {
|
||||
priceProgressRow.appendChild(priceProgressData);
|
||||
priceInfoboxHeadingRow.insertAdjacentElement('afterend', priceProgressRow);
|
||||
|
||||
let token: string | null;
|
||||
|
||||
// Steam Community Market
|
||||
// TODO: Change this to lazy-load, so that it doesn't make network requests when we have cached data.
|
||||
// var steamMarketResults = await getSteamResults(itemName)
|
||||
// logDebug(JSON.stringify(steamMarketResults))
|
||||
|
||||
// Fetch prices.tf access token
|
||||
// https://api2.prices.tf/auth/access
|
||||
try {
|
||||
// throw new Error('dont wanna')
|
||||
token = await getPricesToken();
|
||||
} catch (err) {
|
||||
log('Failed to get an access token for prices.tf: ' + err);
|
||||
}
|
||||
|
||||
let updateTime: Date | null = null;
|
||||
|
||||
enum PriceRowCategory {
|
||||
@@ -210,9 +198,9 @@ async function inject() {
|
||||
// Get current key price
|
||||
let keyPrice: ItemPriceData
|
||||
try {
|
||||
keyPrice = await fetchKeyPrice(token);
|
||||
keyPrice = await fetchKeyPrice();
|
||||
} catch (error) {
|
||||
logError('Failed to get a key price from prices.tf: ' + error);
|
||||
logError('Failed to get a key price from pricedb.io: ' + error);
|
||||
// Footer row
|
||||
const row = document.createElement("tr");
|
||||
|
||||
@@ -220,7 +208,7 @@ async function inject() {
|
||||
label.colSpan = 2;
|
||||
label.style.fontSize = "85%";
|
||||
label.style.textAlign = "center";
|
||||
label.innerHTML = `Failed to get prices from prices.tf. Service may be down.`;
|
||||
label.innerHTML = `Failed to get prices from pricedb.io. Service may be down.`;
|
||||
row.appendChild(label);
|
||||
priceProgressRow.insertAdjacentElement('afterend', row);
|
||||
priceProgressRow.remove();
|
||||
@@ -236,7 +224,7 @@ async function inject() {
|
||||
|
||||
let data: ItemPriceData | null
|
||||
try {
|
||||
data = await fetchPrice(token, itemIndex + ";" + quality, currentTime);
|
||||
data = await fetchPrice(itemIndex + ";" + quality, currentTime);
|
||||
updateTime = new Date(data.update)
|
||||
} catch {
|
||||
log(`${qualifiedName} is unpriced or unavailable, skipping...`)
|
||||
@@ -250,7 +238,7 @@ async function inject() {
|
||||
|
||||
// Check item schema for Australium variant of current defindex
|
||||
if(itemSchema[itemIndex].hasAustraliumVariant) {
|
||||
promises.push(fetchPrice(token, `${itemIndex};11;australium`, currentTime).then(data => {
|
||||
promises.push(fetchPrice(`${itemIndex};11;australium`, currentTime).then(data => {
|
||||
updateTime = new Date(data.update)
|
||||
logDebug(`Saving price for Australium ${itemName}`)
|
||||
|
||||
@@ -278,7 +266,7 @@ async function inject() {
|
||||
festiveHeadingRow.style.display = 'none';
|
||||
festiveHeadingRow.appendChild(festiveHeading);
|
||||
|
||||
promises.push(fetchPrice(token, `${itemSchema[itemIndex].festiveVariant};6`, currentTime).then(data => {
|
||||
promises.push(fetchPrice(`${itemSchema[itemIndex].festiveVariant};6`, currentTime).then(data => {
|
||||
updateTime = new Date(data.update)
|
||||
logDebug(`Saving price for Festive ${itemName}`)
|
||||
|
||||
@@ -290,7 +278,7 @@ async function inject() {
|
||||
logError(error)
|
||||
log(`Festive ${itemName} is unpriced or unavailable, skipping...`)
|
||||
}))
|
||||
promises.push(fetchPrice(token, `${itemSchema[itemIndex].festiveVariant};11`, currentTime).then(data => {
|
||||
promises.push(fetchPrice(`${itemSchema[itemIndex].festiveVariant};11`, currentTime).then(data => {
|
||||
updateTime = new Date(data.update)
|
||||
logDebug(`Saving price for Strange Festive ${itemName}`)
|
||||
|
||||
@@ -328,7 +316,7 @@ async function inject() {
|
||||
case 2: kitIndex = 6523; break;
|
||||
case 3: kitIndex = 6526; break;
|
||||
}
|
||||
promises.push(fetchPrice(token, `${kitIndex};6;uncraftable;kt-${tier};td-${itemIndex}`, currentTime).then(data => {
|
||||
promises.push(fetchPrice(`${kitIndex};6;uncraftable;kt-${tier};td-${itemIndex}`, currentTime).then(data => {
|
||||
updateTime = new Date(data.update)
|
||||
logDebug(`Saving price for ${itemName} Killstreak Kit Tier ${tier}`)
|
||||
|
||||
@@ -369,7 +357,7 @@ async function inject() {
|
||||
itemSchema[itemIndex].botkillerVariants.map((variantIndex) => {
|
||||
const itemName = itemSchema[variantIndex].name
|
||||
const variantName = itemName.includes('Mk.II') ? itemName.split(' ')[0] + ' Mk.II' : itemName.split(' ')[0]
|
||||
promises.push(fetchPrice(token, `${variantIndex};11`, currentTime).then(data => {
|
||||
promises.push(fetchPrice(`${variantIndex};11`, currentTime).then(data => {
|
||||
logDebug(`Saving price for ${itemName}`)
|
||||
updateTime = new Date(data.update)
|
||||
|
||||
@@ -423,7 +411,7 @@ async function inject() {
|
||||
label.style.textAlign = "center";
|
||||
const updateText = $T("Updated %@", locale).replace('%@', updateTime.toLocaleString(locale, { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }))
|
||||
const attributionHeader = $T("Acknowledgements");
|
||||
const pricesAttribution = `<a rel="nofollow" class="external text" href="https://prices.tf">prices.tf</a>`;
|
||||
const pricesAttribution = `<a rel="nofollow" class="external text" href="https://pricedb.io">pricedb.io</a>`;
|
||||
const exchangeRateAttribution = `<a rel="nofollow" class="external text" href="https://www.exchangerate-api.com">Rates By Exchange Rate API</a>`;
|
||||
label.innerHTML = `${updateText}<br><b>${attributionHeader}</b><br>${pricesAttribution}<br>${exchangeRateAttribution}`;
|
||||
row.appendChild(label);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defindex_key, storage_priceprefix } from "./config"
|
||||
import { priceUsingPricesTF } from "./pricing/pricestf"
|
||||
import { priceUsingPricedb } from "./pricing/pricedb"
|
||||
import { getStorageValue, setStorageValue } from "./storage"
|
||||
import { logDebug } from "./utils/log"
|
||||
declare const __ENV_WEBEXTENSION: boolean;
|
||||
@@ -31,17 +31,16 @@ export class ItemPriceData {
|
||||
}
|
||||
|
||||
|
||||
export async function fetchKeyPrice(token: string) {
|
||||
return fetchPrice(token, `${defindex_key};6`, new Date(), 86400000)
|
||||
export async function fetchKeyPrice() {
|
||||
return fetchPrice(`${defindex_key};6`, new Date(), 86400000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a price for a given SKU, using cached values if available.
|
||||
* @param token prices.tf access token.
|
||||
* @param update Date retrieved.
|
||||
* @param ttl Time to cache results in milliseconds. 30 minutes by default.
|
||||
*/
|
||||
export async function fetchPrice(token: string, sku: string, update: Date = new Date(), ttl: number = 30 * 60 * 1000): Promise<ItemPriceData> {
|
||||
export async function fetchPrice(sku: string, update: Date = new Date(), ttl: number = 30 * 60 * 1000): Promise<ItemPriceData> {
|
||||
let data: ItemPriceData | null
|
||||
|
||||
const cached: ItemPriceData = await getStorageValue(storage_priceprefix + sku, null)
|
||||
@@ -51,9 +50,6 @@ export async function fetchPrice(token: string, sku: string, update: Date = new
|
||||
|
||||
if (!data || data.sku != sku || 'update' in data && 'ttl' in data && Date.now() > (new Date(data.update).getTime() + data.ttl)) {
|
||||
logDebug(`Fetching price data for ${sku}`)
|
||||
if(!token || token === '') {
|
||||
throw new Error('No token provided')
|
||||
}
|
||||
data = new ItemPriceData()
|
||||
data.sku = sku
|
||||
data.update = update.getTime()
|
||||
@@ -62,9 +58,9 @@ export async function fetchPrice(token: string, sku: string, update: Date = new
|
||||
try {
|
||||
let response: PricesResponse
|
||||
if(__ENV_USERSCRIPT) {
|
||||
response = await priceUsingPricesTF(token, sku)
|
||||
response = await priceUsingPricedb(sku)
|
||||
} else {
|
||||
response = await chrome.runtime.sendMessage({contentScriptQuery: "priceSKU", service: "prices.tf", sku: sku, token: token});
|
||||
response = await chrome.runtime.sendMessage({contentScriptQuery: "priceSKU", sku: sku});
|
||||
}
|
||||
if (!response || response instanceof Error) {
|
||||
throw new Error(`Bad response: ${response}`)
|
||||
@@ -72,7 +68,7 @@ export async function fetchPrice(token: string, sku: string, update: Date = new
|
||||
data.keys = response.keys
|
||||
data.metal = response.metal
|
||||
} catch (error) {
|
||||
throw new Error(`Received "${error}" error while pricing ${sku} using prices.tf`)
|
||||
throw new Error(`Received "${error}" error while pricing ${sku} using pricedb.io`)
|
||||
}
|
||||
|
||||
if ('metal' in data && 'keys' in data) {
|
||||
@@ -82,4 +78,4 @@ export async function fetchPrice(token: string, sku: string, update: Date = new
|
||||
logDebug(`Using cached price data for ${sku}`)
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
67
src/content/pricing/pricedb.ts
Normal file
67
src/content/pricing/pricedb.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { fetchWrap } from '../fetchWrap'
|
||||
import { logDebug, logError } from '../utils/log'
|
||||
declare const __ENV_WEBEXTENSION: boolean;
|
||||
declare const __ENV_USERSCRIPT: boolean;
|
||||
|
||||
class PricesResponse {
|
||||
keys: number
|
||||
metal: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches current price data for Team Fortress 2 items from pricedb.io.
|
||||
*
|
||||
* This function uses the pricedb.io API to fetch latest pricing data for a given item in keys and metal.
|
||||
* No authentication is required.
|
||||
*
|
||||
* @example
|
||||
* const price = await priceUsingPricedb('105;11');
|
||||
* console.log("Strange Brigade Helm price: ${price.keys} keys ${price.metal} metal")
|
||||
*
|
||||
* @returns {Promise<PricesResponse>} Object containing 'keys' and 'metal' prices
|
||||
* @throws When API returns non-200 status code
|
||||
*/
|
||||
async function priceUsingPricedb(sku: string, retries: number = 3): Promise<PricesResponse> {
|
||||
// pricedb.io
|
||||
// https://pricedb.io/api/item/${sku}
|
||||
try {
|
||||
const response = await fetchWrap(`https://pricedb.io/api/item/${encodeURIComponent(sku)}`, {
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
})
|
||||
if (response.status === 404 && sku.includes(';') && !sku.includes(';uncraftable')) {
|
||||
const quality: number = parseInt(sku.split(';')[1], 10);
|
||||
if(quality === 6) {
|
||||
// Try uncraftable variant if unique weapon
|
||||
return priceUsingPricedb(sku + ';uncraftable');
|
||||
}
|
||||
}
|
||||
if(response.status === 429) {
|
||||
// Happens if we send too many requests (rate limit: 180 req/min)
|
||||
// Retry after a few seconds
|
||||
if(retries >= 0) {
|
||||
logDebug(`Rate limit exceeded, trying again after 1 second, ${retries} retries left`)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return priceUsingPricedb(sku, retries - 1);
|
||||
} else {
|
||||
throw new Error(`Rate limit exceeded, stopping`)
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Pricing request for ${sku} failed with status code: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const prices = new PricesResponse();
|
||||
prices.keys = data.sell.keys
|
||||
prices.metal = data.sell.metal
|
||||
return prices;
|
||||
}
|
||||
catch(error) {
|
||||
logError(`Failed to fetch prices from pricedb.io for item ${sku}`)
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export { priceUsingPricedb, PricesResponse }
|
||||
@@ -1,91 +0,0 @@
|
||||
import { fetchWrap } from '../fetchWrap'
|
||||
import { logDebug, logError } from '../utils/log'
|
||||
declare const __ENV_WEBEXTENSION: boolean;
|
||||
declare const __ENV_USERSCRIPT: boolean;
|
||||
|
||||
async function getPricesToken(): Promise<string> {
|
||||
if(__ENV_USERSCRIPT) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
fetchWrap('https://api2.prices.tf/auth/access', {
|
||||
method: 'post',
|
||||
headers: new Headers({
|
||||
'Accept': 'application/json'
|
||||
})
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status != 200) {
|
||||
reject(response.status)
|
||||
}
|
||||
return response.json()
|
||||
})
|
||||
.then((responseData) => resolve(responseData['accessToken']))
|
||||
})
|
||||
} else {
|
||||
return chrome.runtime.sendMessage({contentScriptQuery: 'getPricesTFToken'})
|
||||
}
|
||||
}
|
||||
|
||||
class PricesResponse {
|
||||
keys: number
|
||||
metal: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current price data for Team Fortress 2 items from prices.tf.
|
||||
*
|
||||
* This function authenticates with the prices.tf API using the provided token,
|
||||
* and uses it to fetch the latest pricing data for the given item in keys and metal.
|
||||
*
|
||||
* @example
|
||||
* const price = await priceUsingPricesTF(token, '105;11');
|
||||
* console.log("Strange Brigade Helm price: ${price.keys} keys ${price.metal} metal")
|
||||
*
|
||||
* @returns {Promise<PricesResponse>} Object containing 'keys' and 'metal' prices
|
||||
* @throws When authentication fails or API returns non-200 status code
|
||||
*/
|
||||
async function priceUsingPricesTF(token: string, sku: string, retries: number = 3): Promise<PricesResponse> {
|
||||
// prices.tf
|
||||
// https://api2.prices.tf/prices/${sku}
|
||||
// Authorization: Bearer ${token}
|
||||
try {
|
||||
const response = await fetchWrap(`https://api2.prices.tf/prices/${encodeURIComponent(sku)}`, {
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
})
|
||||
if (response.status === 404 && sku.includes(';') && !sku.includes(';uncraftable')) {
|
||||
const quality: number = parseInt(sku.split(';')[1], 10);
|
||||
if(quality === 6) {
|
||||
// Try uncraftable variant if unique weapon
|
||||
return priceUsingPricesTF(token, sku + ';uncraftable');
|
||||
}
|
||||
}
|
||||
if(response.status === 503) {
|
||||
// Happens if we send too many requests in a short period of time
|
||||
// Retry after a few seconds
|
||||
if(retries >= 0) {
|
||||
logDebug(`Cloudflare rate limit exceeded, trying again after 1 second, ${retries} retries left`)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return priceUsingPricesTF(token, sku, retries - 1);
|
||||
} else {
|
||||
throw new Error(`Cloudflare rate limit exceeded, stopping`)
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Pricing request for ${sku} failed with status code: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const prices = new PricesResponse();
|
||||
prices.keys = data['sellKeys']
|
||||
prices.metal = data['sellHalfScrap'] / 18.0;
|
||||
return prices;
|
||||
}
|
||||
catch(error) {
|
||||
logError(`Failed to fetch prices from prices.tf for item ${sku}`)
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export { getPricesToken, priceUsingPricesTF, PricesResponse }
|
||||
@@ -10,7 +10,7 @@
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://wiki.teamfortress.com/wiki/*",
|
||||
"https://*.prices.tf/*",
|
||||
"https://*.pricedb.io/*",
|
||||
"https://open.er-api.com/*"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
// @inject-into content
|
||||
// @connect steamcommunity.com
|
||||
// @domain steamcommunity.com
|
||||
// @connect prices.tf
|
||||
// @domain prices.tf
|
||||
// @connect pricedb.io
|
||||
// @domain pricedb.io
|
||||
// @connect open.er-api.com
|
||||
// @domain open.er-api.com
|
||||
// @grant GM.setValue
|
||||
|
||||
Reference in New Issue
Block a user