From 1b6e2b0bd7d036fa8ef1396f2146b82d45d81df9 Mon Sep 17 00:00:00 2001 From: rapture-party Date: Sat, 2 May 2026 15:38:46 -0400 Subject: [PATCH] refactor: replace prices.tf with pricedb.io --- bun.lockb | Bin 317623 -> 323863 bytes src/background/background.ts | 66 +++++++---------------- src/content/content.ts | 90 ++++++++++++++----------------- src/content/priceService.ts | 22 ++++---- src/content/pricing/pricedb.ts | 67 +++++++++++++++++++++++ src/content/pricing/pricestf.ts | 91 -------------------------------- src/manifest.json | 78 +++++++++++++-------------- src/userscript_header.js | 6 +-- 8 files changed, 175 insertions(+), 245 deletions(-) create mode 100644 src/content/pricing/pricedb.ts delete mode 100644 src/content/pricing/pricestf.ts diff --git a/bun.lockb b/bun.lockb index c86c55f3e65e6d46e3637a36bd080eabca5b1978..9f97e780148d3634482cfd4b27159df1c3178b86 100755 GIT binary patch delta 10145 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 delta 10090 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 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==