13 Commits

Author SHA1 Message Date
537c3fe06e test: update tests for new pricing service
All checks were successful
CI / build (push) Successful in 13s
2026-05-02 15:59:24 -04:00
148b2e9c6f chore: bump version to 0.9.0
Some checks failed
CI / build (push) Failing after 8s
2026-05-02 15:39:20 -04:00
1b6e2b0bd7 refactor: replace prices.tf with pricedb.io 2026-05-02 15:38:46 -04:00
0879ede4b8 l10n: update strings
All checks were successful
CI / build (push) Successful in 11s
2026-05-02 15:14:31 -04:00
7cf4b83f4e ci: add bun install steps
All checks were successful
CI / build (push) Successful in 12s
2026-05-02 15:12:21 -04:00
d995052c51 ci: change runner image to ubuntu
Some checks failed
CI / build (push) Failing after 13s
2026-05-01 01:16:08 -04:00
47523d5a6a fix: handle remote prices.tf error in background script 2025-05-18 13:38:23 -04:00
fae3431556 fix: handle key pricing failure 2025-05-18 13:38:04 -04:00
685fc8d766 feat: use source-map devtool on dev builds 2025-05-18 13:37:37 -04:00
894a4de6f1 fix: unhandled state where remote server returns null for given SKU price(s) 2025-05-07 16:45:50 -04:00
0eff663de1 fix: localization dictionary not handling mixed case 2025-05-07 16:44:58 -04:00
ed8a342206 fix: print error when untranslated string found 2025-05-07 16:44:34 -04:00
e6c45c91e3 fix: unhandled error when numbers are null 2025-05-07 16:43:51 -04:00
39 changed files with 275 additions and 320 deletions

View File

@@ -13,11 +13,14 @@ on:
jobs: jobs:
build: build:
runs-on: debian-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4.1.2 uses: actions/checkout@v4.1.2
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Read package version - name: Read package version
uses: tyankatsu0105/read-package-version-actions@v1 uses: tyankatsu0105/read-package-version-actions@v1
id: version id: version

View File

@@ -7,13 +7,16 @@ on:
jobs: jobs:
build: build:
runs-on: debian-latest runs-on: ubuntu-latest
outputs: outputs:
version: ${{ steps.version.outputs.version }} version: ${{ steps.version.outputs.version }}
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4.1.2 uses: actions/checkout@v4.1.2
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Read package version - name: Read package version
uses: tyankatsu0105/read-package-version-actions@v1 uses: tyankatsu0105/read-package-version-actions@v1
id: version id: version
@@ -31,7 +34,7 @@ jobs:
name: tf2wikipricing name: tf2wikipricing
path: dist/ path: dist/
deploy: deploy:
runs-on: debian-latest runs-on: ubuntu-latest
needs: build needs: build
steps: steps:
- name: Download release artifacts - name: Download release artifacts

View File

@@ -1,22 +1,21 @@
import { describe, expect, test, jest, mock, beforeEach } from "bun:test"; import { describe, expect, test, jest, mock, beforeEach } from "bun:test";
import { ItemPriceData, fetchPrice, fetchKeyPrice } from '../src/content/priceService' import { ItemPriceData, fetchPrice, fetchKeyPrice } from '../src/content/priceService'
import { PricesResponse, priceUsingPricesTF } from '../src/content/pricing/pricestf' import { PricesResponse, priceUsingPricedb } from '../src/content/pricing/pricedb'
import { getStorageValue, setStorageValue } from '../src/content/storage' import { getStorageValue, setStorageValue } from '../src/content/storage'
import { defindex_key } from "../src/content/config"; import { defindex_key } from "../src/content/config";
// Mock the storage module // Mock storage module
mock.module('../src/content/storage', () => ({ mock.module('../src/content/storage', () => ({
getStorageValue: jest.fn(), getStorageValue: jest.fn(),
setStorageValue: jest.fn() setStorageValue: jest.fn()
})) }))
// Mock the pricing module // Mock pricing module
mock.module('../src/content/pricing/pricestf', () => ({ mock.module('../src/content/pricing/pricedb', () => ({
priceUsingPricesTF: jest.fn() priceUsingPricedb: jest.fn()
})) }))
describe('Price Service', () => { describe('Price Service', () => {
const mockToken = 'test-token'
const mockDefIndex = 105 // Brigade Helm const mockDefIndex = 105 // Brigade Helm
const mockQuality = 11 // Strange const mockQuality = 11 // Strange
const mockSku = `${mockDefIndex};${mockQuality}` const mockSku = `${mockDefIndex};${mockQuality}`
@@ -49,7 +48,7 @@ describe('Price Service', () => {
test('fetchPrice returns cached data if available and not expired', async () => { test('fetchPrice returns cached data if available and not expired', async () => {
(getStorageValue as jest.Mock).mockResolvedValue(mockCachedData) (getStorageValue as jest.Mock).mockResolvedValue(mockCachedData)
const result = await fetchPrice(mockToken, mockDefIndex + ";" + mockQuality) const result = await fetchPrice(mockDefIndex + ";" + mockQuality)
expect(getStorageValue).toHaveBeenCalledWith(expect.stringContaining(mockSku), null) expect(getStorageValue).toHaveBeenCalledWith(expect.stringContaining(mockSku), null)
expect(result).toEqual(mockCachedData) expect(result).toEqual(mockCachedData)
@@ -58,34 +57,30 @@ describe('Price Service', () => {
test('fetchPrice fetches new data when cache is expired', async () => { test('fetchPrice fetches new data when cache is expired', async () => {
const expiredCache: ItemPriceData = { ...mockCachedData, update: new Date(Date.now() - 2 * mockTtl).getTime() }; const expiredCache: ItemPriceData = { ...mockCachedData, update: new Date(Date.now() - 2 * mockTtl).getTime() };
(getStorageValue as jest.Mock).mockResolvedValue(expiredCache); (getStorageValue as jest.Mock).mockResolvedValue(expiredCache);
(priceUsingPricesTF as jest.Mock).mockResolvedValue(mockPriceResponse); (priceUsingPricedb as jest.Mock).mockResolvedValue(mockPriceResponse);
(chrome.runtime.sendMessage as jest.Fn).mockImplementation(() => mockPriceResponse); (chrome.runtime.sendMessage as jest.Fn).mockImplementation(() => mockPriceResponse);
const result = await fetchPrice(mockToken, mockDefIndex + ";" + mockQuality) const result = await fetchPrice(mockDefIndex + ";" + mockQuality)
expect(setStorageValue).toHaveBeenCalled() expect(setStorageValue).toHaveBeenCalled()
expect(result.metal).not.toBe(mockCachedData.metal) expect(result.metal).not.toBe(mockCachedData.metal)
expect(result.metal).toBe(mockPriceResponse.metal) expect(result.metal).toBe(mockPriceResponse.metal)
}) })
test('fetchPrice rejects with 401 when no token provided', async () => {
await expect(fetchPrice('', mockDefIndex + ";" + mockQuality)).rejects.toThrow()
})
test('fetchPrice handles pricing API errors', async () => { test('fetchPrice handles pricing API errors', async () => {
(chrome.runtime.sendMessage as jest.Fn).mockResolvedValue(null); (chrome.runtime.sendMessage as jest.Fn).mockResolvedValue(null);
(priceUsingPricesTF as jest.Mock).mockImplementation(() => Promise.reject(new Error('500 Internal Server Error'))); (priceUsingPricedb as jest.Mock).mockImplementation(() => Promise.reject(new Error('500 Internal Server Error')));
(getStorageValue as jest.Mock).mockResolvedValue(null) (getStorageValue as jest.Mock).mockResolvedValue(null)
await expect(fetchPrice(mockToken, mockDefIndex + ";" + mockQuality)).rejects.toThrow() await expect(fetchPrice(mockDefIndex + ";" + mockQuality)).rejects.toThrow()
}) })
test('fetchKeyPrice uses correct parameters', async () => { test('fetchKeyPrice uses correct parameters', async () => {
(getStorageValue as jest.Mock).mockResolvedValue(null); (getStorageValue as jest.Mock).mockResolvedValue(null);
(priceUsingPricesTF as jest.Mock).mockResolvedValue(mockKeyPriceResponse); (priceUsingPricedb as jest.Mock).mockResolvedValue(mockKeyPriceResponse);
(chrome.runtime.sendMessage as jest.Fn).mockImplementation(() => mockKeyPriceResponse); (chrome.runtime.sendMessage as jest.Fn).mockImplementation(() => mockKeyPriceResponse);
const result = await fetchKeyPrice(mockToken) const result = await fetchKeyPrice()
expect(result.keys).toBe(0) // A key cannot cost a key :P expect(result.keys).toBe(0) // A key cannot cost a key :P
expect(result.metal).toBe(mockKeyPriceResponse.metal) expect(result.metal).toBe(mockKeyPriceResponse.metal)

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,7 +1,7 @@
{ {
"name": "tf2wikipricing", "name": "tf2wikipricing",
"displayName": "TF2 Wiki Pricing", "displayName": "TF2 Wiki Pricing",
"version": "0.8.1", "version": "0.9.0",
"description": "Adds item pricing to the Team Fortress 2 wiki", "description": "Adds item pricing to the Team Fortress 2 wiki",
"author": "rapture.party", "author": "rapture.party",
"devDependencies": { "devDependencies": {

View File

@@ -27,61 +27,44 @@ 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 { class PricesResponse {
keys: number keys: number
metal: number metal: number
} }
async function priceUsingPricesTF(token: string, sku: string, retries: number = 3): Promise<PricesResponse> { async function priceUsingPricedb(sku: string, retries: number = 3): Promise<PricesResponse> {
const url = `https://api2.prices.tf/prices/${encodeURIComponent(sku)}`; const url = `https://pricedb.io/api/item/${encodeURIComponent(sku)}`;
const response = await fetch(url, { const response = await fetch(url, {
method: 'get', method: 'get',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
} }
}) })
if (response.status === 404 && sku.includes(';') && !sku.includes(';uncraftable')) { if (response.status === 404 && sku.includes(';') && !sku.includes(';uncraftable')) {
const quality: number = parseInt(sku.split(';')[1], 10); const quality: number = parseInt(sku.split(';')[1], 10);
if(quality === 6) { if(quality === 6) {
// Try uncraftable variant if unique weapon // Try uncraftable variant if unique weapon
return priceUsingPricesTF(token, sku + ';uncraftable'); return priceUsingPricedb(sku + ';uncraftable');
} }
} }
if(response.status === 503) { if(response.status === 429) {
// Happens if we send too many requests in a short period of time // Happens if we send too many requests (rate limit: 180 req/min)
// Retry after a few seconds // Retry after a few seconds
if(retries >= 0) { 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)); await new Promise(resolve => setTimeout(resolve, 1000));
return priceUsingPricesTF(token, sku, retries - 1); return priceUsingPricedb(sku, retries - 1);
} else { } else {
throw new Error(`Cloudflare rate limit exceeded, stopping`) 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 data = await response.json();
const prices = new PricesResponse(); const prices = new PricesResponse();
prices.keys = data['sellKeys'] prices.keys = data.sell.keys
prices.metal = data['sellHalfScrap'] / 18.0; prices.metal = data.sell.metal
return prices; return prices;
} }
@@ -89,23 +72,14 @@ chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) { function(request, sender, sendResponse) {
if (request.contentScriptQuery == "priceSKU") { if (request.contentScriptQuery == "priceSKU") {
const sku: string = request.sku const sku: string = request.sku
const service: string = request.service priceUsingPricedb(sku)
const token: string = request.token .then((response) => sendResponse(response))
if(token === "" || !token) { .catch(error => {
sendResponse(new Error("No token provided")) console.error(`Received "${error}" error while pricing ${sku} using pricedb.io`)
sendResponse(null);
return false; return false;
} })
switch (service) {
case "prices.tf": {
priceUsingPricesTF(token, sku)
.then((response) => sendResponse(response))
.catch(error => {
sendResponse(error);
return false;
})
}
}
return true;
} }
return true;
} }
); );

View File

@@ -2,7 +2,6 @@ declare const __ENV_WEBEXTENSION: boolean;
declare const __ENV_USERSCRIPT: boolean; declare const __ENV_USERSCRIPT: boolean;
import { logDebug, log, logError } from './utils/log' import { logDebug, log, logError } from './utils/log'
import { getPricesToken } from './pricing/pricestf'
import itemQualities from 'tf2-static-schema/static/qualities.json'; import itemQualities from 'tf2-static-schema/static/qualities.json';
import { getItemIndexByName, getTradableStatusByDefindex, ItemSchema, ItemSlot, prepareSchema, wipeSchema } from './schemaService' import { getItemIndexByName, getTradableStatusByDefindex, ItemSchema, ItemSlot, prepareSchema, wipeSchema } from './schemaService'
import { $T, extractLocaleFromURL } from './utils/localization' import { $T, extractLocaleFromURL } from './utils/localization'
@@ -16,7 +15,7 @@ let exchangeRates: ExchangeRates | null;
let locale: string = 'en' let locale: string = 'en'
/** Exclude these from the pricelist. */ /** Exclude these from pricelist. */
const excludedQualities = new Set([ const excludedQualities = new Set([
15, // Decorated 15, // Decorated
5, // Unusual 5, // Unusual
@@ -175,22 +174,11 @@ async function inject() {
priceProgressRow.appendChild(priceProgressData); priceProgressRow.appendChild(priceProgressData);
priceInfoboxHeadingRow.insertAdjacentElement('afterend', priceProgressRow); priceInfoboxHeadingRow.insertAdjacentElement('afterend', priceProgressRow);
let token: string | null;
// Steam Community Market // Steam Community Market
// TODO: Change this to lazy-load, so that it doesn't make network requests when we have cached data. // TODO: Change this to lazy-load, so that it doesn't make network requests when we have cached data.
// var steamMarketResults = await getSteamResults(itemName) // var steamMarketResults = await getSteamResults(itemName)
// logDebug(JSON.stringify(steamMarketResults)) // 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; let updateTime: Date | null = null;
enum PriceRowCategory { enum PriceRowCategory {
@@ -208,7 +196,24 @@ async function inject() {
const priceRows: PriceRow[]= []; const priceRows: PriceRow[]= [];
// Get current key price // Get current key price
const keyPrice = await fetchKeyPrice(token); let keyPrice: ItemPriceData
try {
keyPrice = await fetchKeyPrice();
} catch (error) {
logError('Failed to get a key price from pricedb.io: ' + error);
// Footer row
const row = document.createElement("tr");
const label = document.createElement("td");
label.colSpan = 2;
label.style.fontSize = "85%";
label.style.textAlign = "center";
label.innerHTML = `Failed to get prices from pricedb.io. Service may be down.`;
row.appendChild(label);
priceProgressRow.insertAdjacentElement('afterend', row);
priceProgressRow.remove();
return
}
const currentTime = new Date() const currentTime = new Date()
@@ -219,7 +224,7 @@ async function inject() {
let data: ItemPriceData | null let data: ItemPriceData | null
try { try {
data = await fetchPrice(token, itemIndex + ";" + quality, currentTime); data = await fetchPrice(itemIndex + ";" + quality, currentTime);
updateTime = new Date(data.update) updateTime = new Date(data.update)
} catch { } catch {
log(`${qualifiedName} is unpriced or unavailable, skipping...`) log(`${qualifiedName} is unpriced or unavailable, skipping...`)
@@ -233,7 +238,7 @@ async function inject() {
// Check item schema for Australium variant of current defindex // Check item schema for Australium variant of current defindex
if(itemSchema[itemIndex].hasAustraliumVariant) { 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) updateTime = new Date(data.update)
logDebug(`Saving price for Australium ${itemName}`) logDebug(`Saving price for Australium ${itemName}`)
@@ -261,7 +266,7 @@ async function inject() {
festiveHeadingRow.style.display = 'none'; festiveHeadingRow.style.display = 'none';
festiveHeadingRow.appendChild(festiveHeading); 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) updateTime = new Date(data.update)
logDebug(`Saving price for Festive ${itemName}`) logDebug(`Saving price for Festive ${itemName}`)
@@ -273,7 +278,7 @@ async function inject() {
logError(error) logError(error)
log(`Festive ${itemName} is unpriced or unavailable, skipping...`) 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) updateTime = new Date(data.update)
logDebug(`Saving price for Strange Festive ${itemName}`) logDebug(`Saving price for Strange Festive ${itemName}`)
@@ -293,36 +298,36 @@ async function inject() {
itemSchema[itemIndex].slot == ItemSlot.Secondary || itemSchema[itemIndex].slot == ItemSlot.Secondary ||
itemSchema[itemIndex].slot == ItemSlot.Melee) itemSchema[itemIndex].slot == ItemSlot.Melee)
{ {
/// Create subheading /// Create subheading
killstreakKitHeadingRow = document.createElement("tr") killstreakKitHeadingRow = document.createElement("tr")
const heading = document.createElement("th") const heading = document.createElement("th")
heading.className = "infobox-subheader" heading.className = "infobox-subheader"
heading.colSpan = 2 heading.colSpan = 2
heading.innerText = $T("Killstreak Kit") heading.innerText = $T("Killstreak Kit")
heading.style.fontSize = '1em'; heading.style.fontSize = '1em';
heading.style.backgroundColor = '#F5C087'; heading.style.backgroundColor = '#F5C087';
killstreakKitHeadingRow.style.display = 'none'; killstreakKitHeadingRow.style.display = 'none';
killstreakKitHeadingRow.appendChild(heading); killstreakKitHeadingRow.appendChild(heading);
[1,2,3].map((tier) => { [1,2,3].map((tier) => {
let kitIndex: number let kitIndex: number
switch (tier) { switch (tier) {
default: default:
case 1: kitIndex = 6527; break; case 1: kitIndex = 6527; break;
case 2: kitIndex = 6523; break; case 2: kitIndex = 6523; break;
case 3: kitIndex = 6526; 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) updateTime = new Date(data.update)
logDebug(`Saving price for ${itemName} Killstreak Kit Tier ${tier}`) 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 // Silver Mk.I, Gold Mk.II, Rust, Blood, Carbonado, Diamond, Silver Mk.II, Gold Mk.II
@@ -352,7 +357,7 @@ async function inject() {
itemSchema[itemIndex].botkillerVariants.map((variantIndex) => { itemSchema[itemIndex].botkillerVariants.map((variantIndex) => {
const itemName = itemSchema[variantIndex].name const itemName = itemSchema[variantIndex].name
const variantName = itemName.includes('Mk.II') ? itemName.split(' ')[0] + ' Mk.II' : itemName.split(' ')[0] 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}`) logDebug(`Saving price for ${itemName}`)
updateTime = new Date(data.update) updateTime = new Date(data.update)
@@ -404,9 +409,9 @@ async function inject() {
label.colSpan = 2; label.colSpan = 2;
label.style.fontSize = "85%"; label.style.fontSize = "85%";
label.style.textAlign = "center"; 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 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 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>`; 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}`; label.innerHTML = `${updateText}<br><b>${attributionHeader}</b><br>${pricesAttribution}<br>${exchangeRateAttribution}`;
row.appendChild(label); row.appendChild(label);

View File

@@ -1,5 +1,5 @@
import { defindex_key, storage_priceprefix } from "./config" import { defindex_key, storage_priceprefix } from "./config"
import { priceUsingPricesTF } from "./pricing/pricestf" import { priceUsingPricedb } from "./pricing/pricedb"
import { getStorageValue, setStorageValue } from "./storage" import { getStorageValue, setStorageValue } from "./storage"
import { logDebug } from "./utils/log" import { logDebug } from "./utils/log"
declare const __ENV_WEBEXTENSION: boolean; declare const __ENV_WEBEXTENSION: boolean;
@@ -31,17 +31,16 @@ export class ItemPriceData {
} }
export async function fetchKeyPrice(token: string) { export async function fetchKeyPrice() {
return fetchPrice(token, `${defindex_key};6`, new Date(), 86400000) return fetchPrice(`${defindex_key};6`, new Date(), 86400000)
} }
/** /**
* Fetch a price for a given SKU, using cached values if available. * Fetch a price for a given SKU, using cached values if available.
* @param token prices.tf access token.
* @param update Date retrieved. * @param update Date retrieved.
* @param ttl Time to cache results in milliseconds. 30 minutes by default. * @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 let data: ItemPriceData | null
const cached: ItemPriceData = await getStorageValue(storage_priceprefix + sku, 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)) { 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}`) logDebug(`Fetching price data for ${sku}`)
if(!token || token === '') {
throw new Error('No token provided')
}
data = new ItemPriceData() data = new ItemPriceData()
data.sku = sku data.sku = sku
data.update = update.getTime() data.update = update.getTime()
@@ -62,9 +58,9 @@ export async function fetchPrice(token: string, sku: string, update: Date = new
try { try {
let response: PricesResponse let response: PricesResponse
if(__ENV_USERSCRIPT) { if(__ENV_USERSCRIPT) {
response = await priceUsingPricesTF(token, sku) response = await priceUsingPricedb(sku)
} else { } 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) { if (!response || response instanceof Error) {
throw new Error(`Bad response: ${response}`) 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.keys = response.keys
data.metal = response.metal data.metal = response.metal
} catch (error) { } 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) { 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}`) logDebug(`Using cached price data for ${sku}`)
} }
return data return data
} }

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

View File

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

View File

@@ -24,7 +24,7 @@ export function createPriceRow(qualityName: string, data: ItemPriceData, keyPric
const priceLink = document.createElement("span"); const priceLink = document.createElement("span");
let priceString: string = '' let priceString: string = ''
if(data) { if(data && data.keys != null && data.metal != null) {
const gamePrice = formatPrice(data.keys, data.metal, keyPrice.metal, locale).trim() const gamePrice = formatPrice(data.keys, data.metal, keyPrice.metal, locale).trim()
const realPriceUSD = convertTF2PriceToUSD(data.keys, data.metal, keyPrice.metal) const realPriceUSD = convertTF2PriceToUSD(data.keys, data.metal, keyPrice.metal)

View File

@@ -6,6 +6,8 @@ function toFixed(num: number, fixed: number) {
} }
export function formatPrice(keys: number, metal: number, keyPrice: number, locale: string = 'en') { export function formatPrice(keys: number, metal: number, keyPrice: number, locale: string = 'en') {
if(keys == null) keys = 0
if(metal == null) metal = 0
const formattedKeys = +(keys + (metal / keyPrice)).toFixed(2) const formattedKeys = +(keys + (metal / keyPrice)).toFixed(2)
let output: string = '' let output: string = ''

View File

@@ -1,40 +1,40 @@
import { logDebug } from "./log"; import { logDebug, logError } from "./log";
const localizations: Record<string, object> = { const localizations: Record<string, object> = {
'en': require('../../strings/en'), // English
'es': require('../../strings/es'), // Spanish
'ja': require('../../strings/ja'), // Japanese
'it': require('../../strings/it'), // Italian
'ar': require('../../strings/ar'), // Arabic 'ar': require('../../strings/ar'), // Arabic
'cs': require('../../strings/cs'), // Czech 'cs': require('../../strings/cs'), // Czech
'da': require('../../strings/da'), // Danish 'da': require('../../strings/da'), // Danish
'de': require('../../strings/de'), // German 'nl': require('../../strings/nl'), // Dutch
'fi': require('../../strings/fi'), // Finnish 'fi': require('../../strings/fi'), // Finnish
'fr': require('../../strings/fr'), // French 'fr': require('../../strings/fr'), // French
'en': require('../../strings/en'), // English
'hu': require('../../strings/hu'), // Hungarian 'hu': require('../../strings/hu'), // Hungarian
'it': require('../../strings/it'), // Italian
'ja': require('../../strings/ja'), // Japanese
'de': require('../../strings/de'), // German
'ko': require('../../strings/ko'), // Korean 'ko': require('../../strings/ko'), // Korean
'nl': require('../../strings/nl'), // Dutch
'no': require('../../strings/no'), // Norwegian Bokmål 'no': require('../../strings/no'), // Norwegian Bokmål
'pl': require('../../strings/pl'), // Polish 'pl': require('../../strings/pl'), // Polish
'pt': require('../../strings/pt'), // Portuguese 'pt': require('../../strings/pt'), // Portuguese
'pt-BR': require('../../strings/pt-BR'), // Brazilian Portuguese 'pt-br': require('../../strings/pt-BR'), // Brazilian Portuguese
'ro': require('../../strings/ro'), // Romanian 'ro': require('../../strings/ro'), // Romanian
'zh-hans': require('../../strings/zh-Hans'), // Simplified Chinese
'ru': require('../../strings/ru'), // Russian 'ru': require('../../strings/ru'), // Russian
'es': require('../../strings/es'), // Spanish
'sv': require('../../strings/sv'), // Swedish 'sv': require('../../strings/sv'), // Swedish
'zh-hant': require('../../strings/zh-Hant'), // Traditional Chinese
'tr': require('../../strings/tr'), // Turkish 'tr': require('../../strings/tr'), // Turkish
'zh-Hans': require('../../strings/zh-Hans'), // Simplified Chinese
'zh-Hant': require('../../strings/zh-Hant'), // Traditional Chinese
} }
export function $T(s: string, locale?: Intl.LocalesArgument): string { export function $T(s: string, locale?: Intl.LocalesArgument): string {
const code = locale ? locale.toString() : extractLocaleFromURL(document.URL) const code = locale ? locale.toString() : extractLocaleFromURL(document.URL)
if (code in localizations) { if (code in localizations) {
const translation = localizations[code] as Record<string, string>; const translation = localizations[code.toLowerCase()] as Record<string, string>;
const result = translation[s] ?? s; const result = translation[s] ?? s;
logDebug(`Translating "${s}" to locale "${code}": ${result}`); logDebug(`Translating "${s}" to locale "${code}": ${result}`);
return result; return result;
} }
logDebug(`Untranslated string "${s}" in locale "${code}`); logError(`Untranslated string "${s}" in locale "${code}"`);
return s; return s;
} }

View File

@@ -10,7 +10,7 @@
], ],
"host_permissions": [ "host_permissions": [
"https://wiki.teamfortress.com/wiki/*", "https://wiki.teamfortress.com/wiki/*",
"https://*.prices.tf/*", "https://*.pricedb.io/*",
"https://open.er-api.com/*" "https://open.er-api.com/*"
], ],
"web_accessible_resources": [ "web_accessible_resources": [

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "تم التحديث يوم %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "البيانات غير متوفرة", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Aktualizováno %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Data nejsou k dispozici", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Opdateret %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Data utilgængelige", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Daten nicht verfügbar", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,7 +5,7 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings

View File

@@ -5,7 +5,7 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Precios de la comunidad", "Community Pricing": "Precios de la comunidad",
// Itembox footer // Itembox footer
"Updated %@.": "Actualizado %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Actualizado %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Agradecimientos", // sourced from AppleGlot "Acknowledgements": "Agradecimientos", // sourced from AppleGlot
// Price strings // Price strings

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Dataa ei saatavilla", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Données non disponibles", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Adat nem érhető el", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Prezzo Comunitario", "Community Pricing": "Prezzo Comunitario",
// Itembox footer // Itembox footer
"Updated %@.": "Aggiornato il %@.", "Updated %@": "Aggiornato il %@",
"Acknowledgements": "Note legali", // sourced from AppleGlot "Acknowledgements": "Note legali", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Dati non disponibili", // sourced from AppleGlot
"%@ ref": "%@ raf", "%@ ref": "%@ raf",
"%@ key": "%@ chiave", "%@ key": "%@ chiave",
"%@ keys": "%@ chiavi", "%@ keys": "%@ chiavi",

View File

@@ -5,7 +5,7 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "共同体価格", "Community Pricing": "共同体価格",
// Itembox footer // Itembox footer
"Updated %@.": "アップデート: %@", // %@ is a date string, sourced from AppleGlot "Updated %@": "アップデート: %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "謝辞", // sourced from AppleGlot "Acknowledgements": "謝辞", // sourced from AppleGlot
// Price strings // Price strings

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "데이터를 사용할 수 없음", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Bijgewerkt %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Gegevens niet beschikbaar", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Data ikke tilgjengelig", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Dane niedostępne", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Atualizado: %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Dados indisponíveis", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Dados indisponíveis", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Date indisponibile", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,7 +5,7 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Цены сообщества", "Community Pricing": "Цены сообщества",
// Itembox footer // Itembox footer
"Updated %@.": "Обновлено %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Обновлено %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Уведомления", // sourced from AppleGlot "Acknowledgements": "Уведомления", // sourced from AppleGlot
// Price strings // Price strings

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Data ej tillgängliga", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "Veri yok", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "数据不可用", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -5,11 +5,11 @@ module.exports = {
// Itembox header // Itembox header
"Community Pricing": "Community Pricing", "Community Pricing": "Community Pricing",
// Itembox footer // Itembox footer
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot "Updated %@": "Updated %@", // %@ is a date string, sourced from AppleGlot
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot "Acknowledgements": "Acknowledgements", // sourced from AppleGlot
// Price strings // Price strings
"Data unavailable": "Data unavailable", // sourced from AppleGlot "Data unavailable": "無法使用資料", // sourced from AppleGlot
"%@ ref": "%@ ref", "%@ ref": "%@ ref",
"%@ key": "%@ key", "%@ key": "%@ key",
"%@ keys": "%@ keys", "%@ keys": "%@ keys",

View File

@@ -8,8 +8,8 @@
// @inject-into content // @inject-into content
// @connect steamcommunity.com // @connect steamcommunity.com
// @domain steamcommunity.com // @domain steamcommunity.com
// @connect prices.tf // @connect pricedb.io
// @domain prices.tf // @domain pricedb.io
// @connect open.er-api.com // @connect open.er-api.com
// @domain open.er-api.com // @domain open.er-api.com
// @grant GM.setValue // @grant GM.setValue

View File

@@ -22,9 +22,10 @@ const defines = {
__VERSION__: JSON.stringify(package.version), __VERSION__: JSON.stringify(package.version),
} }
module.exports = [ module.exports = (env, argv) => [
// WebExtension // WebExtension
{ {
devtool: argv.mode === 'production' ? false : 'source-map',
entry: { entry: {
content: './src/content/content.ts', content: './src/content/content.ts',
background: './src/background/background.ts', background: './src/background/background.ts',