import styleCss from './style.css' import { logDebug, log, logError } from './utils/log' import { getPricesToken, priceUsingPricesTF } from './pricing/pricestf' import itemQualities from 'tf2-static-schema/static/qualities.json'; import { getItemIndexByName, getTradableStatusByDefindex, ItemSchema, prepareSchema, wipeSchema } from './schemaService' import { $T, extractLocaleFromURL } from './utils/localization' import { fetchPrice, fetchKeyPrice, ItemPriceData } from './priceService' import { createPriceRow, createStoreButton } from './uiRenderer' import { findFirstElement, findFirstChildElement } from './utils/dom' var itemSchema: ItemSchema | null; var pageLocale: string = 'en' /** Exclude these from the pricelist. */ const excludedQualities = new Set([ 15, // Decorated 5, // Unusual ]); // Helper functions function getKeyByValue(object: any, value: string) { return Object.keys(object).find(key => object[key] === value); } function extractPageTitleFromURL(url: string): string { var split = url.substring(url.indexOf("/wiki/") + "/wiki/".length); if (split.indexOf('/') != -1) { // Remove language suffix (/es) split = split.substring(0, split.indexOf('/')); } return decodeURIComponent(split.replaceAll('_', ' ')); } // Main function async function inject() { const itemInfobox = findFirstElement('.item-infobox'); if (!itemInfobox) { // Not an item page return; } const locale = extractLocaleFromURL(document.URL); var itemIndex: number | null = null; var itemName: string | null = null; // Try using buy buttons, if they exist const buyButton = findFirstChildElement('.btn_buynow', itemInfobox); const marketButton = findFirstChildElement('.btn_buynow_market', itemInfobox); if(buyButton) { const link = (buyButton.parentElement as HTMLLinkElement); if(link && link.href) { itemIndex = parseInt(link.href.replace('https://store.steampowered.com/buyitem/440/', '')); } } if(!itemIndex && marketButton) { const link = (marketButton.parentElement as HTMLLinkElement); if(link && link.href) { itemIndex = parseInt(link.href.replace('https://steamcommunity.com/market/search/?q=appid:440+prop_def_index:', '')); } } // Try using item name const header = findFirstChildElement('.infobox-header', itemInfobox); if (!itemIndex && header) { // Get from document.body const canonical = document.querySelector('link[rel="canonical"]'); if (canonical && canonical instanceof HTMLLinkElement) { const url = canonical.href; if (url.indexOf("/wiki/") != -1) { itemName = extractPageTitleFromURL(url); } } const url = document.URL; if (itemName && !itemIndex) { itemIndex = getItemIndexByName(itemSchema, itemName) } } if (!itemIndex) { // Cannot continue without index logError(itemName ? `Could not find defindex for ${itemName}` : `Could not determine item defindex or name`); return; } if(!itemName) { // Get name from index itemName = (itemSchema[itemIndex.toString()] as any).name; } log(`Starting lookup for ${itemName} (defindex ${itemIndex})`); if(getTradableStatusByDefindex(itemSchema, itemIndex) == false) { log(`${itemName} is not tradable, exiting`) return; } var qualities: number[] = [] const firstQualityTag = findFirstChildElement('.quality-tag', itemInfobox); for (const qualityTag of Array.from(firstQualityTag.parentElement.children)) { const link = findFirstChildElement('a', qualityTag) as HTMLLinkElement; if (!link) { continue; } const qualityName = extractPageTitleFromURL(link.href); const quality = parseInt(getKeyByValue(itemQualities, qualityName)); if (!quality) { continue; } qualities.push(quality); } /// 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) // ... <- We want to insert our button here. // th.infobox-header (Basic Information) // ... var storeButtons: HTMLTableRowElement[] = []; // backpack.tf button storeButtons.push(createStoreButton("backpack.tf", new URL(`https://backpack.tf/classifieds?item=${encodeURIComponent(itemName)}`))); // mannco.store button storeButtons.push(createStoreButton("mannco.store", new URL(`https://mannco.store/tf2?&search=${encodeURIComponent(itemName)}&page=1`))); // marketplace.tf button // Disabled due to requiring login // storeButtons.push(createStoreButton("marketplace.tf", new URL(`https://marketplace.tf/browse/tf2?sterm=${encodeURIComponent(itemName)}`))); // stntrading.eu button storeButtons.push(createStoreButton("stntrading.eu", new URL(`https://stntrading.eu/item/tf2/${encodeURIComponent(itemName)}`))); const headers = itemInfobox.querySelectorAll("th.infobox-header"); storeButtons.reverse().forEach(element => { if (marketButton) { marketButton.closest("tr").insertAdjacentElement('afterend', element); } else if (buyButton) { buyButton.closest("tr").insertAdjacentElement('afterend', element); } else if (headers.length > 2) { headers[1].closest("tr").insertAdjacentElement('beforebegin', element); } else { itemInfobox.children[0].appendChild(element); } }); /// Create price infobox const priceInfoboxHeadingRow = document.createElement("tr") const priceInfoboxHeading = document.createElement("th") priceInfoboxHeading.className = "infobox-header" priceInfoboxHeading.colSpan = 2 priceInfoboxHeading.innerText = $T("Community Pricing") priceInfoboxHeadingRow.appendChild(priceInfoboxHeading); headers[1].closest("tr").insertAdjacentElement('beforebegin', priceInfoboxHeadingRow); // Create progress bar const priceProgressRow = document.createElement("tr") const priceProgressData = document.createElement("td") priceProgressData.colSpan = 2 const priceProgress = document.createElement("progress"); priceProgress.id = "tf2wikipricing"; priceProgress.style.width = "75%" priceProgress.style.marginLeft = "12.5%" priceProgressData.appendChild(priceProgress); priceProgressRow.appendChild(priceProgressData); priceInfoboxHeadingRow.insertAdjacentElement('afterend', priceProgressRow); var 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); } var updateTime: Date | null = null; interface PriceRow { quality: number row: HTMLTableRowElement } var priceRows: PriceRow[]= []; // Get current key price const keyPrice = await fetchKeyPrice(token); var currentTime = new Date() const promises = qualities.filter(x => !excludedQualities.has(x)).map(async (quality) => { const qualifiedName = ((quality != 6 ? itemQualities[quality as unknown as keyof typeof itemQualities].toString() : '') + ' ' + itemName).trim() // logDebug(`Fetching price for ${qualifiedName}`) var data: ItemPriceData | null try { data = await fetchPrice(token, itemIndex, quality, currentTime); updateTime = new Date(data.update) } catch { log(`${qualifiedName} is unpriced or unavailable, skipping...`) } const qualityName = itemQualities[quality as unknown as keyof typeof itemQualities].toString() const priceRow = createPriceRow(qualityName, data, keyPrice, locale) priceRows.push({quality: quality, row: priceRow}) }) Promise.all(promises).then(() => { priceRows.sort((a, b) => { // Sort 6 first always, then numerically if (a.quality === 6) { return -1; } else if (b.quality === 6) { return 1; } else { return a.quality == b.quality ? a.quality < b.quality ? -1 : 1 : 0; } }).reverse().forEach((element) => { priceInfoboxHeadingRow.insertAdjacentElement('afterend', element.row); }) if(!updateTime) { updateTime = new Date() } // Footer row const row = document.createElement("tr"); const label = document.createElement("td"); label.colSpan = 2; label.style.fontSize = "85%"; const updateText = $T("Updated %@.", locale).replace('%@', updateTime.toLocaleString(locale, { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' })) const attributionText = $T("Trade prices sourced from %@. Currency conversions are approximate.", locale).replace('%@', 'prices.tf'); label.innerHTML = `${updateText}
${attributionText}`; row.appendChild(label); priceProgressRow.insertAdjacentElement('afterend', row); priceProgressRow.remove() }) } function addStyles() { const head = document.head || document.getElementsByTagName('head')[0], style = document.createElement('style'); head.appendChild(style); style.innerHTML = styleCss; } prepareSchema().then(function (schema) { itemSchema = schema; if (!itemSchema) { logError("No item schema ready, exiting."); wipeSchema(); // FIXME: ugly hack. requires additional page reload. if prepareSchema returns null, we should handle it properly return; } pageLocale = extractLocaleFromURL(document.URL) addStyles(); inject(); // TODO: Purge expired price data });