diff --git a/bun.lockb b/bun.lockb index c86c55f..9f97e78 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/background/background.ts b/src/background/background.ts index 86a1789..047e58b 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -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 { - const url = `https://api2.prices.tf/prices/${encodeURIComponent(sku)}`; +async function priceUsingPricedb(sku: string, retries: number = 3): Promise { + 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")) + priceUsingPricedb(sku) + .then((response) => sendResponse(response)) + .catch(error => { + console.error(`Received "${error}" error while pricing ${sku} using pricedb.io`) + sendResponse(null); return false; - } - switch (service) { - case "prices.tf": { - priceUsingPricesTF(token, sku) - .then((response) => sendResponse(response)) - .catch(error => { - console.error(`Received "${error}" error while pricing ${sku} using prices.tf`) - sendResponse(null); - return false; - }) - } - } - return true; + }) } + return true; } -); \ No newline at end of file +); diff --git a/src/content/content.ts b/src/content/content.ts index ab5e4b4..0c1f6ad 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -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 @@ -118,7 +117,7 @@ async function inject() { /// Create buttons // Item infobox is a table with the following layout: - // + // // th.infobox-header (Item Name) // tr (3D item viewer/2D preview image) // tr (buy buttons if applicable) @@ -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}`) @@ -310,36 +298,36 @@ async function inject() { itemSchema[itemIndex].slot == ItemSlot.Secondary || itemSchema[itemIndex].slot == ItemSlot.Melee) { - /// Create subheading - killstreakKitHeadingRow = document.createElement("tr") - const heading = document.createElement("th") - heading.className = "infobox-subheader" - heading.colSpan = 2 - heading.innerText = $T("Killstreak Kit") - heading.style.fontSize = '1em'; - heading.style.backgroundColor = '#F5C087'; - killstreakKitHeadingRow.style.display = 'none'; - killstreakKitHeadingRow.appendChild(heading); - [1,2,3].map((tier) => { - let kitIndex: number - switch (tier) { - default: - case 1: kitIndex = 6527; break; - case 2: kitIndex = 6523; break; - case 3: kitIndex = 6526; break; - } - promises.push(fetchPrice(token, `${kitIndex};6;uncraftable;kt-${tier};td-${itemIndex}`, currentTime).then(data => { - updateTime = new Date(data.update) - logDebug(`Saving price for ${itemName} Killstreak Kit Tier ${tier}`) + /// Create subheading + killstreakKitHeadingRow = document.createElement("tr") + const heading = document.createElement("th") + heading.className = "infobox-subheader" + heading.colSpan = 2 + heading.innerText = $T("Killstreak Kit") + heading.style.fontSize = '1em'; + heading.style.backgroundColor = '#F5C087'; + killstreakKitHeadingRow.style.display = 'none'; + killstreakKitHeadingRow.appendChild(heading); + [1,2,3].map((tier) => { + let kitIndex: number + switch (tier) { + default: + case 1: kitIndex = 6527; break; + case 2: kitIndex = 6523; break; + case 3: kitIndex = 6526; break; + } + 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}`) - const priceRow = createPriceRow($T(`kt-${tier}`), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Killstreak_Kit") + const priceRow = createPriceRow($T(`kt-${tier}`), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Killstreak_Kit") - priceRows.push({order: tier, row: priceRow, category: PriceRowCategory.KillstreakKit}) + priceRows.push({order: tier, row: priceRow, category: PriceRowCategory.KillstreakKit}) + }) + .catch((error) => { + logError(`Failed to fetch price for ${itemName} Killstreak Kit Tier ${tier}`, error) + })) }) - .catch((error) => { - logError(`Failed to fetch price for ${itemName} Killstreak Kit Tier ${tier}`, error) - })) - }) } // Silver Mk.I, Gold Mk.II, Rust, Blood, Carbonado, Diamond, Silver Mk.II, Gold Mk.II @@ -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 = `prices.tf`; + const pricesAttribution = `pricedb.io`; const exchangeRateAttribution = `Rates By Exchange Rate API`; label.innerHTML = `${updateText}
${attributionHeader}
${pricesAttribution}
${exchangeRateAttribution}`; row.appendChild(label); @@ -460,4 +448,4 @@ prepareSchema() }) .catch((error) => { logError(error); -}) \ No newline at end of file +}) diff --git a/src/content/priceService.ts b/src/content/priceService.ts index fac3b1a..8b6901f 100644 --- a/src/content/priceService.ts +++ b/src/content/priceService.ts @@ -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; @@ -21,7 +21,7 @@ export class ItemPriceData { scmPrice: number toString(): string { - return `Price for ${this.sku}, fetched ${new Date(this.update)} (expires ${new Date(this.update + this.ttl)})\n` + + return `Price for ${this.sku}, fetched ${new Date(this.update)} (expires ${new Date(this.update + this.ttl)})\n` + JSON.stringify({ keys: this.keys, metal: this.metal, @@ -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 { +export async function fetchPrice(sku: string, update: Date = new Date(), ttl: number = 30 * 60 * 1000): Promise { 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 -} + } diff --git a/src/content/pricing/pricedb.ts b/src/content/pricing/pricedb.ts new file mode 100644 index 0000000..d7419f0 --- /dev/null +++ b/src/content/pricing/pricedb.ts @@ -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} Object containing 'keys' and 'metal' prices + * @throws When API returns non-200 status code + */ +async function priceUsingPricedb(sku: string, retries: number = 3): Promise { + // 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 } diff --git a/src/content/pricing/pricestf.ts b/src/content/pricing/pricestf.ts deleted file mode 100644 index abeff72..0000000 --- a/src/content/pricing/pricestf.ts +++ /dev/null @@ -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 { - if(__ENV_USERSCRIPT) { - return new Promise((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} 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 { - // 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 } \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index 0d5fd6e..62ddfaf 100755 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,39 +1,39 @@ -{ - "name": EXTENSION_NAME, - "description": EXTENSION_DESCRIPTION, - "author": EXTENSION_AUTHOR, - "manifest_version": 3, - "version": EXTENSION_VERSION, - "permissions": [ - "storage", - "scripting" - ], - "host_permissions": [ - "https://wiki.teamfortress.com/wiki/*", - "https://*.prices.tf/*", - "https://open.er-api.com/*" - ], - "web_accessible_resources": [ - { - "resources": ["lib/style.css", "resources/*"], - "matches": ["https://wiki.teamfortress.com/*"] - } - ], - "content_scripts": [ - { - "matches": ["*://wiki.teamfortress.com/wiki/*"], - "run_at": "document_start", - "all_frames": true, - "css": ["lib/style.css"], - "js": ["content/content.js"] - } - ], - "background": { - "service_worker": "background/background.js", - "type": "module" - }, - "icons": { - "48": "icons/icon-48.png", - "96": "icons/icon-96.png" - } -} +{ + "name": EXTENSION_NAME, + "description": EXTENSION_DESCRIPTION, + "author": EXTENSION_AUTHOR, + "manifest_version": 3, + "version": EXTENSION_VERSION, + "permissions": [ + "storage", + "scripting" + ], + "host_permissions": [ + "https://wiki.teamfortress.com/wiki/*", + "https://*.pricedb.io/*", + "https://open.er-api.com/*" + ], + "web_accessible_resources": [ + { + "resources": ["lib/style.css", "resources/*"], + "matches": ["https://wiki.teamfortress.com/*"] + } + ], + "content_scripts": [ + { + "matches": ["*://wiki.teamfortress.com/wiki/*"], + "run_at": "document_start", + "all_frames": true, + "css": ["lib/style.css"], + "js": ["content/content.js"] + } + ], + "background": { + "service_worker": "background/background.js", + "type": "module" + }, + "icons": { + "48": "icons/icon-48.png", + "96": "icons/icon-96.png" + } +} diff --git a/src/userscript_header.js b/src/userscript_header.js index 11d0f31..2702446 100644 --- a/src/userscript_header.js +++ b/src/userscript_header.js @@ -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 @@ -18,4 +18,4 @@ // @grant GM_getValue // @grant GM.xmlhttpRequest // @grant GM_xmlhttpRequest -// ==/UserScript== \ No newline at end of file +// ==/UserScript==