From 53cc4d92dd40365fb3607fc95f7b9663e95c1c29 Mon Sep 17 00:00:00 2001 From: GODrums Date: Sun, 27 Aug 2023 06:26:22 +0200 Subject: [PATCH 1/7] Skinport extension architecture draft --- manifest.json | 7 ++++- src/content_script.ts | 42 ++------------------------- src/skinport/content_script.ts | 53 ++++++++++++++++++++++++++++++++++ src/util/extensionsettings.ts | 43 +++++++++++++++++++++++++++ tsconfig.json | 2 +- webpack/webpack.common.js | 7 +++-- 6 files changed, 109 insertions(+), 45 deletions(-) create mode 100644 src/skinport/content_script.ts create mode 100644 src/util/extensionsettings.ts diff --git a/manifest.json b/manifest.json index a818d59..787bf94 100644 --- a/manifest.json +++ b/manifest.json @@ -12,7 +12,7 @@ "content_scripts": [ { "matches": ["*://*.csfloat.com/*", "*://*.csgofloat.com/*"], - "js": ["js/content_script.js"], + "js": ["js/csfloat/content_script.js"], "css": ["css/stylesheet.css"], "run_at": "document_end" }, @@ -20,6 +20,11 @@ "matches": ["*://*.csfloat.com/*", "*://*.csgofloat.com/*"], "js": ["js/injectionhandler.js"], "run_at": "document_start" + }, + { + "matches": ["*://*.skinport.com/*"], + "js": ["js/skinport/content_script.js"], + "run_at": "document_end" } ], "icons": { diff --git a/src/content_script.ts b/src/content_script.ts index b2e530a..dbf233e 100644 --- a/src/content_script.ts +++ b/src/content_script.ts @@ -13,6 +13,7 @@ import { loadBuffMapping, loadMapping, } from './mappinghandler'; +import { initSettings } from './util/extensionsettings'; type PriceResult = { price_difference: number; @@ -28,7 +29,7 @@ async function init() { // this has to be done as first thing to not miss timed events activateHandler(); - await initSettings(); + extensionSettings = await initSettings(); await loadMapping(); await loadBuffMapping(); @@ -69,45 +70,6 @@ async function firstLaunch() { } } -async function initSettings() { - extensionSettings = {}; - chrome.storage.local.get((data) => { - if (data.buffprice) { - extensionSettings.buffprice = Boolean(data.buffprice); - } - if (data.autorefresh) { - extensionSettings.autorefresh = Boolean(data.autorefresh); - } - if (data.priceReference) { - extensionSettings.priceReference = data.priceReference as ExtensionSettings['priceReference']; - } - if (data.refreshInterval) { - extensionSettings.refreshInterval = data.refreshInterval as ExtensionSettings['refreshInterval']; - } - if (data.showSteamPrice) { - extensionSettings.showSteamPrice = Boolean(data.showSteamPrice); - } - if (data.stickerPrices) { - extensionSettings.stickerPrices = Boolean(data.stickerPrices); - } - if (data.listingAge) { - extensionSettings.listingAge = Number(data.listingAge) as ExtensionSettings['listingAge']; - } - if (data.showBuffDifference) { - extensionSettings.showBuffDifference = Boolean(data.showBuffDifference); - } - if (data.showTopButton) { - extensionSettings.showTopButton = Boolean(data.showTopButton); - } - }); - - // wait for settings to be loaded, takes about 1.5 seconds - await new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 1500); - }); -} function parseHTMLString(htmlString: string, container: HTMLElement) { let parser = new DOMParser(); diff --git a/src/skinport/content_script.ts b/src/skinport/content_script.ts new file mode 100644 index 0000000..d444787 --- /dev/null +++ b/src/skinport/content_script.ts @@ -0,0 +1,53 @@ +import { ExtensionSettings } from '../@typings/FloatTypes'; +import { + loadBuffMapping, + loadMapping, +} from '../mappinghandler'; +import { initSettings } from '../util/extensionsettings'; + +async function init() { + //get current url + let url = window.location.href; + if (!url.includes('skinport.com')) { + return; + } + console.log('[BetterFloat] Starting BetterFloat'); + // catch the events thrown by the script + // this has to be done as first thing to not miss timed events + // activateHandler(); + + extensionSettings = await initSettings(); + await loadMapping(); + await loadBuffMapping(); + + // if (extensionSettings.showTopButton) { + // createTopButton(); + // } + + + // //check if url is in supported subpages + // if (url.endsWith('float.com/')) { + // await firstLaunch(); + // } else { + // for (let i = 0; i < supportedSubPages.length; i++) { + // if (url.includes(supportedSubPages[i])) { + // await firstLaunch(); + // } + // } + // } + + // mutation observer is only needed once + // if (!isObserverActive) { + // console.debug('[BetterFloat] Starting observer'); + // await applyMutation(); + // console.log('[BetterFloat] Observer started'); + + // isObserverActive = true; + // } +} + +let extensionSettings: ExtensionSettings; +let runtimePublicURL = chrome.runtime.getURL('../public'); +// mutation observer active? +let isObserverActive = false; +init(); \ No newline at end of file diff --git a/src/util/extensionsettings.ts b/src/util/extensionsettings.ts new file mode 100644 index 0000000..897f86c --- /dev/null +++ b/src/util/extensionsettings.ts @@ -0,0 +1,43 @@ +import { ExtensionSettings } from "../@typings/FloatTypes"; + +export async function initSettings(): Promise { + let extensionSettings = {}; + chrome.storage.local.get((data) => { + if (data.buffprice) { + extensionSettings.buffprice = Boolean(data.buffprice); + } + if (data.autorefresh) { + extensionSettings.autorefresh = Boolean(data.autorefresh); + } + if (data.priceReference) { + extensionSettings.priceReference = data.priceReference as ExtensionSettings['priceReference']; + } + if (data.refreshInterval) { + extensionSettings.refreshInterval = data.refreshInterval as ExtensionSettings['refreshInterval']; + } + if (data.showSteamPrice) { + extensionSettings.showSteamPrice = Boolean(data.showSteamPrice); + } + if (data.stickerPrices) { + extensionSettings.stickerPrices = Boolean(data.stickerPrices); + } + if (data.listingAge) { + extensionSettings.listingAge = Number(data.listingAge) as ExtensionSettings['listingAge']; + } + if (data.showBuffDifference) { + extensionSettings.showBuffDifference = Boolean(data.showBuffDifference); + } + if (data.showTopButton) { + extensionSettings.showTopButton = Boolean(data.showTopButton); + } + }); + + // wait for settings to be loaded, takes about 1.5 seconds + await new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 1500); + }); + + return extensionSettings; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index edd02dd..677ebf3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,5 @@ "exactOptionalPropertyTypes": true, }, "exclude": ["node_modules"], - "include": ["src/*.ts"] + "include": ["src/*.ts", "src/util/extensionsettings.ts"] } diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index fb8f200..77fdb58 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -7,11 +7,12 @@ module.exports = { entry: { popup: path.join(srcDir, 'popup.ts'), background: path.join(srcDir, 'background.ts'), - content_script: path.join(srcDir, 'content_script.ts'), + csfloat_script: { import: path.join(srcDir, 'content_script.ts'), filename: 'csfloat/content_script.js' }, inject: path.join(srcDir, 'inject.ts'), injectionhandler: path.join(srcDir, 'injectionhandler.ts'), - eventhandler: path.join(srcDir, 'eventhandler.ts'), - mappinghandler: path.join(srcDir, 'mappinghandler.ts'), + // eventhandler: path.join(srcDir, 'eventhandler.ts'), + // mappinghandler: path.join(srcDir, 'mappinghandler.ts'), + skinport_content: { import: path.join(srcDir, 'skinport/content_script.ts'), filename: 'skinport/content_script.js' }, }, output: { path: path.join(__dirname, "../dist/js"), From 9cfadcd2e86e7909a664fa02862589d09f0f42fa Mon Sep 17 00:00:00 2001 From: GODrums Date: Sun, 27 Aug 2023 07:58:17 +0200 Subject: [PATCH 2/7] Added types for Skinport --- manifest.json | 12 ++-- src/@typings/FloatTypes.d.ts | 103 +++++++++++++++++++++++++++- src/{ => csfloat}/content_script.ts | 8 +-- src/eventhandler.ts | 18 ++++- src/inject.ts | 3 +- src/skinport/content_script.ts | 3 +- tsconfig.json | 2 +- webpack/webpack.common.js | 2 +- 8 files changed, 133 insertions(+), 18 deletions(-) rename src/{ => csfloat}/content_script.ts (99%) diff --git a/manifest.json b/manifest.json index 787bf94..58c0459 100644 --- a/manifest.json +++ b/manifest.json @@ -1,11 +1,11 @@ { "name": "BetterFloat", "author": "Rums", - "version": "1.3.2", - "version_name": "1.3.2", - "description": "Enhance your experience on CSFloat.com", + "version": "1.4.0", + "version_name": "1.4.0", + "description": "Enhance your experience on CSFloat.com and Skinport.com", "manifest_version": 3, - "host_permissions": ["*://prices.csgotrader.app/*", "*://csfloat.com/*"], + "host_permissions": ["*://prices.csgotrader.app/*", "*://*.csfloat.com/*", "*://*.skinport.com/*"], "background": { "service_worker": "js/background.js" }, @@ -17,7 +17,7 @@ "run_at": "document_end" }, { - "matches": ["*://*.csfloat.com/*", "*://*.csgofloat.com/*"], + "matches": ["*://*.csfloat.com/*", "*://*.csgofloat.com/*", "*://*.skinport.com/*"], "js": ["js/injectionhandler.js"], "run_at": "document_start" }, @@ -35,7 +35,7 @@ }, "permissions": ["storage"], "web_accessible_resources": [ { - "matches": [ "https://csfloat.com/*", "https://csgofloat.com/*" ], + "matches": [ "https://csfloat.com/*", "https://csgofloat.com/*", "*://*.skinport.com/*" ], "resources": [ "public/buff_favicon.png", "public/clock-solid.svg", "public/chevron-up-solid.svg", "js/inject.js"] } ] } diff --git a/src/@typings/FloatTypes.d.ts b/src/@typings/FloatTypes.d.ts index 5b9b6ad..f9bb747 100644 --- a/src/@typings/FloatTypes.d.ts +++ b/src/@typings/FloatTypes.d.ts @@ -69,6 +69,107 @@ export type CSGOTraderMapping = { }; }; +export module Skinport { + export type MarketData = { + filter: { + components: { + appid: number; + data: any; + name: string; + type: string; + }[]; + total: number; + }; + items: Item[]; + message: string | null; + requestId: string; + success: boolean; + } + + export type CartData = { + message: string | null; + requestId: string; + success: boolean; + result: { + cart: Item[]; + openOrders: any[]; // what is this? + }; + } + + export type Item = { + appid: number; + assetId: number; + assetid: string; // those are not the same! + bgColor: string | null; + canHaveScreenshots: boolean; + category: string; + category_localized: string; + collection: string | null; + collection_localized: string | null; + color: string; + currency: string; + customName: string | null; + exterior: string; + family: string; + family_localized: string; + finish: number; + id: number; + image: string; + itemId: number; + link: string; + lock: any | null; + marketHashName: string; + marketName: string; + name: string; + ownItem: boolean; + pattern: number; + productId: number; + quality: string; // e.g. "★" + rarity: string; // e.g. "Covert" + rarityColor: string; // in hex + rarity_localized: string; + saleId: number; + salePrice: number; // e.g. 148936 for 1,489.36€ + saleStatus: string; + saleType: string; + screenshots: string[] | null; + shortId: string; + souvenir: boolean; + stackAble: boolean; + stattrak: boolean; + steamid: string; + stickers: StickerData[]; + subCategory: string; + subCategory_localized: string; + suggestedPrice: number; + tags: [{ + name: string; + name_localized: string; + }]; + text: string; + title: string; + type: string; + url: string; + version: string; + versionType: string; + wear: number; + }; + + export type StickerData = { + color: string | null; + img: string; + name: string; + name_localized: string; + slot: number; + slug: any | null; + sticker_id: number | null; + type: string | null; + type_localized: string | null; + value: number | null; + wear: number | null; + }; +} + export interface EventData { status: string; url: string; @@ -101,7 +202,7 @@ export type ListingData = { rarity: number; rarity_name: string; scm: SCMType; - stickers: [StickerData]; + stickers: StickerData[]; tradable: boolean; type: 'skin' | 'sticker'; type_name: 'Skin' | 'Sticker'; diff --git a/src/content_script.ts b/src/csfloat/content_script.ts similarity index 99% rename from src/content_script.ts rename to src/csfloat/content_script.ts index dbf233e..275a577 100644 --- a/src/content_script.ts +++ b/src/csfloat/content_script.ts @@ -1,7 +1,7 @@ // Official documentation: https://developer.chrome.com/docs/extensions/mv3/content_scripts/ -import { ExtensionSettings, FloatItem, HistoryData, ItemCondition, ItemStyle, ListingData } from './@typings/FloatTypes'; -import { activateHandler } from './eventhandler'; +import { ExtensionSettings, FloatItem, HistoryData, ItemCondition, ItemStyle, ListingData } from '../@typings/FloatTypes'; +import { activateHandler } from '../eventhandler'; import { getBuffMapping, getInventoryHelperPrice, @@ -12,8 +12,8 @@ import { handleSpecialStickerNames, loadBuffMapping, loadMapping, -} from './mappinghandler'; -import { initSettings } from './util/extensionsettings'; +} from '../mappinghandler'; +import { initSettings } from '../util/extensionsettings'; type PriceResult = { price_difference: number; diff --git a/src/eventhandler.ts b/src/eventhandler.ts index a57433f..06e4bf4 100644 --- a/src/eventhandler.ts +++ b/src/eventhandler.ts @@ -1,4 +1,4 @@ -import { EventData, HistoryData, ListingData, SellerData } from './@typings/FloatTypes'; +import { EventData, HistoryData, ListingData, SellerData, Skinport } from './@typings/FloatTypes'; import { cacheHistory, cacheItems } from './mappinghandler'; type StallData = { @@ -10,12 +10,24 @@ export function activateHandler() { // important: https://stackoverflow.com/questions/9515704/access-variables-and-functions-defined-in-page-context-using-a-content-script/9517879#9517879 document.addEventListener('BetterFloat_INTERCEPTED_REQUEST', function (e) { var eventData = (e).detail; - processEvent(eventData); + //switch depending on current site + if (window.location.href.includes('csfloat.com')) { + processCSFloatEvent(eventData); + } else if (window.location.href.includes('skinport.com')) { + processSkinportEvent(eventData); + } }); } +function processSkinportEvent(eventData: EventData) { + console.debug('[BetterFloat] Received data from url: ' + eventData.url + ', data:', eventData.data); + if (eventData.url.includes('api/browse/730')) { + // Skinport.MarketData + } +} + // process intercepted data -function processEvent(eventData: EventData) { +function processCSFloatEvent(eventData: EventData) { console.debug('[BetterFloat] Received data from url: ' + eventData.url + ', data:', eventData.data); if (eventData.url.includes('v1/listings?')) { cacheItems(eventData.data as ListingData[]); diff --git a/src/inject.ts b/src/inject.ts index b03f3bb..f21f3d4 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -10,7 +10,8 @@ function openIntercept() { window.XMLHttpRequest.prototype.open = function () { (this).addEventListener('load', (e) => { let target = e.currentTarget; - if (!target.responseURL.includes('csfloat.com')) { + console.debug('[BetterFloat] Intercepted HTTP request to: ' + target.responseURL); + if (!target.responseURL.includes('csfloat.com') && !target.responseURL.includes('skinport.com')) { console.debug('[BetterFloat] Ignoring HTTP request to: ' + target.responseURL); return; } diff --git a/src/skinport/content_script.ts b/src/skinport/content_script.ts index d444787..196cea9 100644 --- a/src/skinport/content_script.ts +++ b/src/skinport/content_script.ts @@ -3,6 +3,7 @@ import { loadBuffMapping, loadMapping, } from '../mappinghandler'; +import { activateHandler } from '../eventhandler'; import { initSettings } from '../util/extensionsettings'; async function init() { @@ -14,7 +15,7 @@ async function init() { console.log('[BetterFloat] Starting BetterFloat'); // catch the events thrown by the script // this has to be done as first thing to not miss timed events - // activateHandler(); + activateHandler(); extensionSettings = await initSettings(); await loadMapping(); diff --git a/tsconfig.json b/tsconfig.json index 677ebf3..86ead53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,5 @@ "exactOptionalPropertyTypes": true, }, "exclude": ["node_modules"], - "include": ["src/*.ts", "src/util/extensionsettings.ts"] + "include": ["src/*.ts", "src/util/extensionsettings.ts", "src/csfloat/content_script.ts", "src/eventhandler.ts"] } diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index 77fdb58..686d504 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -7,7 +7,7 @@ module.exports = { entry: { popup: path.join(srcDir, 'popup.ts'), background: path.join(srcDir, 'background.ts'), - csfloat_script: { import: path.join(srcDir, 'content_script.ts'), filename: 'csfloat/content_script.js' }, + csfloat_script: { import: path.join(srcDir, 'csfloat/content_script.ts'), filename: 'csfloat/content_script.js' }, inject: path.join(srcDir, 'inject.ts'), injectionhandler: path.join(srcDir, 'injectionhandler.ts'), // eventhandler: path.join(srcDir, 'eventhandler.ts'), From 53aaa3bb71c93bfd2ad4ebccb0538e5a72e6220a Mon Sep 17 00:00:00 2001 From: GODrums Date: Sun, 27 Aug 2023 08:22:04 +0200 Subject: [PATCH 3/7] Websocket listener for Skinport --- manifest.json | 4 ++-- src/@typings/FloatTypes.d.ts | 35 ++++++++++++++++++++++++++++++++++ src/inject.ts | 1 - src/skinport/content_script.ts | 17 +++++++++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index 58c0459..c005523 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "version_name": "1.4.0", "description": "Enhance your experience on CSFloat.com and Skinport.com", "manifest_version": 3, - "host_permissions": ["*://prices.csgotrader.app/*", "*://*.csfloat.com/*", "*://*.skinport.com/*"], + "host_permissions": ["*://prices.csgotrader.app/*", "*://*.csfloat.com/*", "*://*.skinport.com/*", "wss://skinport.com/*"], "background": { "service_worker": "js/background.js" }, @@ -33,7 +33,7 @@ "action": { "default_popup": "/html/index.html" }, - "permissions": ["storage"], + "permissions": ["storage", "webRequest"], "web_accessible_resources": [ { "matches": [ "https://csfloat.com/*", "https://csgofloat.com/*", "*://*.skinport.com/*" ], "resources": [ "public/buff_favicon.png", "public/clock-solid.svg", "public/chevron-up-solid.svg", "js/inject.js"] diff --git a/src/@typings/FloatTypes.d.ts b/src/@typings/FloatTypes.d.ts index f9bb747..5eb8c7b 100644 --- a/src/@typings/FloatTypes.d.ts +++ b/src/@typings/FloatTypes.d.ts @@ -86,6 +86,41 @@ export module Skinport { success: boolean; } + // https://skinport.com/api/home + export type HomeData = { + message: string | null; + requestId: string; + success: boolean; + adverts: { + bgColor: string; + button: { + text: string; + link: string; + }; + id: number; + img: string; + img2x: string; + text: string; + title: string; + }[]; + blog: { + img: string; + published_at: string; + slug: string; + title: string; + }[]; + sales: { + appid: number; + items: Item[]; + total: number; + }[]; // the four sale rows + score: { + count: number; + rating: number; + }; // Trustpilot score + }; + + // https://skinport.com/api/cart export type CartData = { message: string | null; requestId: string; diff --git a/src/inject.ts b/src/inject.ts index f21f3d4..4fc7b21 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -10,7 +10,6 @@ function openIntercept() { window.XMLHttpRequest.prototype.open = function () { (this).addEventListener('load', (e) => { let target = e.currentTarget; - console.debug('[BetterFloat] Intercepted HTTP request to: ' + target.responseURL); if (!target.responseURL.includes('csfloat.com') && !target.responseURL.includes('skinport.com')) { console.debug('[BetterFloat] Ignoring HTTP request to: ' + target.responseURL); return; diff --git a/src/skinport/content_script.ts b/src/skinport/content_script.ts index 196cea9..640d8a1 100644 --- a/src/skinport/content_script.ts +++ b/src/skinport/content_script.ts @@ -16,6 +16,9 @@ async function init() { // catch the events thrown by the script // this has to be done as first thing to not miss timed events activateHandler(); + + // start intercepting websocket requests + webSocketListener(); extensionSettings = await initSettings(); await loadMapping(); @@ -47,6 +50,20 @@ async function init() { // } } +// Idea from: https://stackoverflow.com/a/53990245 +function webSocketListener() { + const networkFilters = { + urls: [ + "wss://skinport.com/socket.io/?EIO=4&transport=websocket" + ] + }; + chrome.webRequest.onCompleted.addListener((details) => { + const { tabId, requestId } = details; + console.log('[BetterFloat] Intercepted websocket request: ', details); + // do stuff here + }, networkFilters); +} + let extensionSettings: ExtensionSettings; let runtimePublicURL = chrome.runtime.getURL('../public'); // mutation observer active? From 8b06942c1bd0ca08ca103ebcb6c955ca264d8bcb Mon Sep 17 00:00:00 2001 From: GODrums Date: Mon, 28 Aug 2023 05:20:26 +0200 Subject: [PATCH 4/7] Basic Skinport features --- manifest.json | 4 +- src/@typings/FloatTypes.d.ts | 13 ++ src/Injectionhandler.ts | 11 ++ src/background.ts | 13 ++ src/csfloat/content_script.ts | 13 +- src/inject.ts | 2 +- src/mappinghandler.ts | 30 ++-- src/skinport/content_script.ts | 265 +++++++++++++++++++++++++++++---- src/util/helperfunctions.ts | 9 ++ src/websocketlistener.js | 43 ++++++ tsconfig.json | 2 +- webpack/webpack.common.js | 6 +- 12 files changed, 350 insertions(+), 61 deletions(-) create mode 100644 src/util/helperfunctions.ts create mode 100644 src/websocketlistener.js diff --git a/manifest.json b/manifest.json index c005523..58c0459 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "version_name": "1.4.0", "description": "Enhance your experience on CSFloat.com and Skinport.com", "manifest_version": 3, - "host_permissions": ["*://prices.csgotrader.app/*", "*://*.csfloat.com/*", "*://*.skinport.com/*", "wss://skinport.com/*"], + "host_permissions": ["*://prices.csgotrader.app/*", "*://*.csfloat.com/*", "*://*.skinport.com/*"], "background": { "service_worker": "js/background.js" }, @@ -33,7 +33,7 @@ "action": { "default_popup": "/html/index.html" }, - "permissions": ["storage", "webRequest"], + "permissions": ["storage"], "web_accessible_resources": [ { "matches": [ "https://csfloat.com/*", "https://csgofloat.com/*", "*://*.skinport.com/*" ], "resources": [ "public/buff_favicon.png", "public/clock-solid.svg", "public/chevron-up-solid.svg", "js/inject.js"] diff --git a/src/@typings/FloatTypes.d.ts b/src/@typings/FloatTypes.d.ts index 5eb8c7b..952ab5e 100644 --- a/src/@typings/FloatTypes.d.ts +++ b/src/@typings/FloatTypes.d.ts @@ -131,6 +131,19 @@ export module Skinport { }; } + export type Listing = { + name: string; + type: string; + text: string; + price: number; + stickers: { + name: string; + }[]; + style: ItemStyle; + wear: number; + wear_name: string; + } + export type Item = { appid: number; assetId: number; diff --git a/src/Injectionhandler.ts b/src/Injectionhandler.ts index 9c3eec4..e075ebc 100644 --- a/src/Injectionhandler.ts +++ b/src/Injectionhandler.ts @@ -1,4 +1,5 @@ injectScript(); +// injectWebsocketListener(); // inject script into page function injectScript() { @@ -9,3 +10,13 @@ function injectScript() { }; (document.head || document.documentElement).appendChild(script); } + +// can listen to Skinport websocket wss stream +// function injectWebsocketListener() { +// let script = document.createElement('script'); +// script.src = chrome.runtime.getURL('js/websocketlistener.js'); +// script.onload = function () { +// (this).remove(); +// }; +// (document.head || document.documentElement).appendChild(script); +// } \ No newline at end of file diff --git a/src/background.ts b/src/background.ts index 6d665b1..47f0d2d 100644 --- a/src/background.ts +++ b/src/background.ts @@ -41,3 +41,16 @@ if (lastUpdate < Date.now() - 1000 * 60 * 60 * 8) { lastUpdate = Date.now(); chrome.storage.local.set({ lastUpdate: lastUpdate }); } + +// Idea from: https://stackoverflow.com/a/53990245 +// the WSS Skinport stream contains the LIVE feed. Does not work yet. +// chrome.webRequest.onBeforeSendHeaders.addListener( +// (details) => { +// const { tabId, requestId } = details; +// console.log('[BetterFloat] Intercepted websocket request: ', details); +// // do stuff here +// }, +// { +// urls: ['wss://skinport.com/socket.io/?EIO=4&transport=websocket'], +// } +// ); diff --git a/src/csfloat/content_script.ts b/src/csfloat/content_script.ts index 275a577..1d7a50b 100644 --- a/src/csfloat/content_script.ts +++ b/src/csfloat/content_script.ts @@ -14,6 +14,7 @@ import { loadMapping, } from '../mappinghandler'; import { initSettings } from '../util/extensionsettings'; +import { parseHTMLString } from '../util/helperfunctions'; type PriceResult = { price_difference: number; @@ -70,17 +71,6 @@ async function firstLaunch() { } } - -function parseHTMLString(htmlString: string, container: HTMLElement) { - let parser = new DOMParser(); - let doc = parser.parseFromString(htmlString, 'text/html'); - const tags = doc.getElementsByTagName(`body`)[0]; - - for (const tag of tags.children) { - container.appendChild(tag); - } -} - async function refreshButton() { const matChipList = document.querySelector('.mat-chip-list-wrapper'); @@ -600,3 +590,4 @@ let lastRefresh = 0; let isObserverActive = false; init(); + diff --git a/src/inject.ts b/src/inject.ts index 4fc7b21..53b03ae 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -62,4 +62,4 @@ function openIntercept() { return open.apply(this, arguments); }; -} +} \ No newline at end of file diff --git a/src/mappinghandler.ts b/src/mappinghandler.ts index 7378d1b..ddb67b2 100644 --- a/src/mappinghandler.ts +++ b/src/mappinghandler.ts @@ -96,6 +96,13 @@ export function handleSpecialStickerNames(name: string): string { return name; } +type SIHRelay = { + name: string; + data: SteaminventoryhelperResponse; + strategy: string; + time: string; +}; + type SteaminventoryhelperResponse = { success: boolean; items: { @@ -108,26 +115,21 @@ type SteaminventoryhelperResponse = { }; }; +// Currently using my own server to relay requests to steaminventoryhelper due to their CORS policy +// For security reasons, I cannot share my whole server code, but the relevant endpoint is here: +// https://gist.github.com/GODrums/5b2d24c17c136a1b37acd14b1089933c export async function getInventoryHelperPrice(buff_name: string): Promise { if (cachedInventoryHelperResponses[buff_name]) { console.log(`[BetterFloat] Returning cached steaminventoryhelper response for ${buff_name}: `, cachedInventoryHelperResponses[buff_name]); return cachedInventoryHelperResponses[buff_name]?.items[buff_name]?.buff163?.price ?? null; } console.log(`[BetterFloat] Attempting to get price for ${buff_name} from steaminventoryhelper`); - return await fetch('https://api.steaminventoryhelper.com/v2/live-prices/getPrices', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - appId: 730, - markets: ['buff163'], - items: [buff_name], - }), - }).then((response) => response.json()).then((data: SteaminventoryhelperResponse) => { - console.log(`[BetterFloat] Steaminventoryhelper response for ${buff_name}: `, data); - cachedInventoryHelperResponses[buff_name] = data; - return data?.items[buff_name]?.buff163?.price; + return await fetch('https://api.rums.dev/sih/'+encodeURI(buff_name)).then((response) => response.json()).then((response: SIHRelay) => { + console.log(`[BetterFloat] Steaminventoryhelper response for ${buff_name}: `, response); + if (response.data) { + cachedInventoryHelperResponses[buff_name] = response.data; + } + return response.data?.items[buff_name]?.buff163?.price; }).catch((err) => { console.error(err); return null; diff --git a/src/skinport/content_script.ts b/src/skinport/content_script.ts index 640d8a1..b8380f0 100644 --- a/src/skinport/content_script.ts +++ b/src/skinport/content_script.ts @@ -1,10 +1,8 @@ -import { ExtensionSettings } from '../@typings/FloatTypes'; -import { - loadBuffMapping, - loadMapping, -} from '../mappinghandler'; +import { ExtensionSettings, ItemStyle, Skinport } from '../@typings/FloatTypes'; +import { getBuffMapping, getInventoryHelperPrice, getPriceMapping, handleSpecialStickerNames, loadBuffMapping, loadMapping } from '../mappinghandler'; import { activateHandler } from '../eventhandler'; import { initSettings } from '../util/extensionsettings'; +import { parseHTMLString } from '../util/helperfunctions'; async function init() { //get current url @@ -17,18 +15,10 @@ async function init() { // this has to be done as first thing to not miss timed events activateHandler(); - // start intercepting websocket requests - webSocketListener(); - extensionSettings = await initSettings(); await loadMapping(); await loadBuffMapping(); - // if (extensionSettings.showTopButton) { - // createTopButton(); - // } - - // //check if url is in supported subpages // if (url.endsWith('float.com/')) { // await firstLaunch(); @@ -41,31 +31,246 @@ async function init() { // } // mutation observer is only needed once - // if (!isObserverActive) { - // console.debug('[BetterFloat] Starting observer'); - // await applyMutation(); - // console.log('[BetterFloat] Observer started'); + if (!isObserverActive) { + console.debug('[BetterFloat] Starting observer'); + await applyMutation(); + console.log('[BetterFloat] Observer started'); + + isObserverActive = true; + } +} + +async function applyMutation() { + let observer = new MutationObserver(async (mutations) => { + if (extensionSettings.buffprice) { + for (let mutation of mutations) { + for (let i = 0; i < mutation.addedNodes.length; i++) { + let addedNode = mutation.addedNodes[i]; + // some nodes are not elements, so we need to check + if (!(addedNode instanceof HTMLElement)) continue; + + // console.log('[BetterFloat] Mutation observer triggered, added node:', addedNode); + + if (addedNode.className && addedNode.className.toString().includes('CatalogPage-item')) { + await adjustItem(addedNode); + } + // item popout + // if (addedNode.tagName && addedNode.tagName.toLowerCase() == 'item-detail') { + // await adjustItem(addedNode, true); + // // item from listings + // } else if (addedNode.className && addedNode.className.toString().includes('flex-item')) { + // await adjustItem(addedNode); + // } + } + } + } + }); + observer.observe(document, { childList: true, subtree: true }); +} - // isObserverActive = true; +async function adjustItem(container: Element) { + const item = getFloatItem(container); + await addBuffPrice(item, container); + // if (extensionSettings.stickerPrices) { + // await addStickerInfo(item, container, cachedItem, priceResult); + // } + // if (extensionSettings.listingAge > 0) { + // await addListingAge(item, container, cachedItem); // } } -// Idea from: https://stackoverflow.com/a/53990245 -function webSocketListener() { - const networkFilters = { - urls: [ - "wss://skinport.com/socket.io/?EIO=4&transport=websocket" - ] +function getFloatItem(container: Element): Skinport.Listing { + let name = container.querySelector('.ItemPreview-itemName')?.textContent ?? ''; + let price = Number(container.querySelector('.ItemPreview-price .Tooltip-link')?.innerHTML.substring(1).replace(',', '')) ?? 0; + let type = container.querySelector('.ItemPreview-itemTitle')?.textContent ?? ''; + let text = container.querySelector('.ItemPreview-itemText')?.innerHTML ?? ''; + + let style: ItemStyle = ''; + if (name.includes('Doppler')) { + style = name.split('(')[1].split(')')[0] as ItemStyle; + } else if (name.includes('Vanilla')) { + style = 'Vanilla'; + } + + let stickers: { name: string }[] = []; + let stickersDiv = container.querySelector('.ItemPreview-stickers'); + if (stickersDiv) { + for (let sticker of stickersDiv.children) { + let stickerName = sticker.children[0].getAttribute('alt'); + if (stickerName) { + stickers.push({ + name: stickerName, + }); + } + } + } + const getWear = (wearDiv: HTMLElement) => { + let wear = ''; + + if (wearDiv) { + let w = Number(wearDiv.innerHTML); + if (w < 0.07) { + wear = 'Factory New'; + } else if (w < 0.15) { + wear = 'Minimal Wear'; + } else if (w < 0.38) { + wear = 'Field-Tested'; + } else if (w < 0.45) { + wear = 'Well-Worn'; + } else { + wear = 'Battle-Scarred'; + } + } + return wear; + }; + let wearDiv = container.querySelector('.WearBar-value'); + let wear = wearDiv ? getWear(wearDiv as HTMLElement) : ''; + return { + name: name, + price: price, + type: type, + text: text, + stickers: stickers, + style: style, + wear: Number(wearDiv?.innerHTML), + wear_name: wear, }; - chrome.webRequest.onCompleted.addListener((details) => { - const { tabId, requestId } = details; - console.log('[BetterFloat] Intercepted websocket request: ', details); - // do stuff here - }, networkFilters); +} + +async function addBuffPrice(item: Skinport.Listing, container: Element): Promise { + await loadMapping(); + let buff_name = handleSpecialStickerNames(createBuffName(item)); + let priceMapping = await getPriceMapping(); + let helperPrice: number | null = null; + + if (!priceMapping[buff_name] || !priceMapping[buff_name]['buff163'] || !priceMapping[buff_name]['buff163']['starting_at'] || !priceMapping[buff_name]['buff163']['highest_order']) { + console.debug(`[BetterFloat] No price mapping found for ${buff_name}`); + helperPrice = await getInventoryHelperPrice(buff_name); + } + + let buff_id = await getBuffMapping(buff_name); + // we cannot use the getItemPrice function here as it does not return the correct price for doppler skins + let priceListing = 0; + let priceOrder = 0; + if (typeof helperPrice == 'number') { + priceListing = helperPrice; + priceOrder = helperPrice; + } else { + if (item.style != '' && item.style != 'Vanilla') { + priceListing = priceMapping[buff_name]['buff163']['starting_at']['doppler'][item.style]; + priceOrder = priceMapping[buff_name]['buff163']['highest_order']['doppler'][item.style]; + } else { + priceListing = priceMapping[buff_name]['buff163']['starting_at']['price']; + priceOrder = priceMapping[buff_name]['buff163']['highest_order']['price']; + } + } + if (priceListing == undefined) { + priceListing = 0; + } + if (priceOrder == undefined) { + priceOrder = 0; + } + + //TODO: from here + + const presentationDiv = container.querySelector('.ItemPreview-mainAction'); + if (presentationDiv) { + let buffLink = document.createElement('a'); + buffLink.className = 'ItemPreview-sideAction betterskinport-bufflink'; + buffLink.style.width = '60px'; + buffLink.target = '_blank'; + buffLink.innerText = 'Buff'; + if (buff_id > 0) { + buffLink.href = `https://buff.163.com/goods/${buff_id}`; + } else { + buffLink.href = `https://buff.163.com/market/csgo#tab=selling&page_num=1&search=${encodeURIComponent(buff_name)}`; + } + if (!presentationDiv.querySelector('.betterskinport-bufflink')) { + presentationDiv.after(buffLink); + } + } + + let priceDiv = container.querySelector('.ItemPreview-oldPrice'); + if (priceDiv && !container.querySelector('.betterfloat-buffprice')) { + priceDiv.className += 'betterfloat-buffprice'; + let buffContainer = document.createElement('div'); + let buffImage = document.createElement('img'); + buffImage.setAttribute('src', runtimePublicURL + '/buff_favicon.png'); + buffImage.setAttribute('style', 'height: 20px; margin-right: 5px'); + buffContainer.appendChild(buffImage); + let buffPrice = document.createElement('div'); + buffPrice.setAttribute('class', 'suggested-price betterfloat-buffprice'); + let tooltipSpan = document.createElement('span'); + tooltipSpan.setAttribute('class', 'betterfloat-buff-tooltip'); + tooltipSpan.textContent = 'Bid: Highest buy order price; Ask: Lowest listing price'; + buffPrice.appendChild(tooltipSpan); + let buffPriceBid = document.createElement('span'); + buffPriceBid.setAttribute('style', 'color: orange;'); + buffPriceBid.textContent = `Bid $${priceOrder}`; + buffPrice.appendChild(buffPriceBid); + let buffPriceDivider = document.createElement('span'); + buffPriceDivider.setAttribute('style', 'color: gray;margin: 0 3px 0 3px;'); + buffPriceDivider.textContent = '|'; + buffPrice.appendChild(buffPriceDivider); + let buffPriceAsk = document.createElement('span'); + buffPriceAsk.setAttribute('style', 'color: greenyellow;'); + buffPriceAsk.textContent = `Ask $${priceListing}`; + buffPrice.appendChild(buffPriceAsk); + buffContainer.appendChild(buffPrice); + if (extensionSettings.showSteamPrice) { + let divider = document.createElement('div'); + priceDiv.after(buffContainer); + priceDiv.after(divider); + } else { + priceDiv.replaceWith(buffContainer); + } + } + + if (extensionSettings.showBuffDifference) { + const difference = item.price - (extensionSettings.priceReference == 0 ? priceOrder : priceListing); + const priceContainer = container.querySelector('.ItemPreview-discount'); + let saleTag = priceContainer.firstChild; + if (saleTag) { + priceContainer.removeChild(saleTag); + } + if (item.price !== 0) { + const buffPriceHTML = ` ${ + difference == 0 ? '-$0' : (difference > 0 ? '+$' : '-$') + Math.abs(difference).toFixed(2) + } `; + parseHTMLString(buffPriceHTML, priceContainer); + } + } +} + +function createBuffName(item: Skinport.Listing): string { + // let full_name = `${item.name}`; + // if (item.type.includes('Sticker')) { + // full_name = `Sticker | ` + full_name; + // } else if (!item.type.includes('Container')) { + // if (item.type.includes('StatTrak') || item.type.includes('Souvenir')) { + // full_name = full_name.includes('★') ? `★ StatTrak™ ${full_name.split('★ ')[1]}` : `${item.quality} ${full_name}`; + // } + // if (item.style != 'Vanilla') { + // full_name += ` (${item.condition})`; + // } + // } + // return full_name + // .replace(/ +(?= )/g, '') + // .replace(/\//g, '-') + // .trim(); + let full_name = `${(item.text.includes('Knife') || item.text.includes('Gloves')) && !item.text.includes('StatTrak') ? '★ ' : ''}${item.type}${ + item.name.includes('Vanilla') ? '' : ' | ' + item.name.split(' (')[0].trim() + }${item.name.includes('Vanilla') ? '' : ' (' + item.wear + ')'}`; + if (item.name.includes('Dragon King')) full_name = `M4A4 | 龍王 (Dragon King)${' (' + item.wear + ')'}`; + else if (item.text.includes('Container') || item.text.includes('Collectible') || item.text.includes('Graffiti')) full_name = item.name; + else if (item.text.includes('Sticker')) full_name = `Sticker | ${item.name}`; + else if (item.text.includes('Patch')) full_name = `Patch | ${item.name}`; + else if (item.text.includes('Agent')) full_name = `${name} | ${item.type}`; + return full_name.replace(/ +(?= )/g, '').replace(/\//g, '-'); } let extensionSettings: ExtensionSettings; let runtimePublicURL = chrome.runtime.getURL('../public'); // mutation observer active? let isObserverActive = false; -init(); \ No newline at end of file +init(); diff --git a/src/util/helperfunctions.ts b/src/util/helperfunctions.ts new file mode 100644 index 0000000..d3c0431 --- /dev/null +++ b/src/util/helperfunctions.ts @@ -0,0 +1,9 @@ +export function parseHTMLString(htmlString: string, container: HTMLElement) { + let parser = new DOMParser(); + let doc = parser.parseFromString(htmlString, 'text/html'); + const tags = doc.getElementsByTagName(`body`)[0]; + + for (const tag of tags.children) { + container.appendChild(tag); + } +} \ No newline at end of file diff --git a/src/websocketlistener.js b/src/websocketlistener.js new file mode 100644 index 0000000..e576802 --- /dev/null +++ b/src/websocketlistener.js @@ -0,0 +1,43 @@ +(function () { + var OrigWebSocket = window.WebSocket; + + var callWebSocket = OrigWebSocket.apply.bind(OrigWebSocket); + var wsAddListener = OrigWebSocket.prototype.addEventListener; + wsAddListener = wsAddListener.call.bind(wsAddListener); + window.WebSocket = function WebSocket(url, protocols) { + var ws; + if (!(this instanceof WebSocket)) { + // Called without 'new' (browsers will throw an error). + ws = callWebSocket(this, arguments); + } else if (arguments.length === 1) { + ws = new OrigWebSocket(url); + } else if (arguments.length >= 2) { + ws = new OrigWebSocket(url, protocols); + } else { // No arguments (browsers will throw an error) + ws = new OrigWebSocket(); + } + + wsAddListener(ws, 'message', function (event) { + console.log("Received:", event); + }); + wsAddListener(ws, 'open', function (event) { + console.log("Open:", event); + }); + wsAddListener(ws, 'close', function (event) { + console.log("Close:", event); + }); + wsAddListener(ws, 'error', function (event) { + console.log("Error:", event); + }); + return ws; + }.bind(); + window.WebSocket.prototype = OrigWebSocket.prototype; + window.WebSocket.prototype.constructor = window.WebSocket; + + var wsSend = OrigWebSocket.prototype.send; + wsSend = wsSend.apply.bind(wsSend); + OrigWebSocket.prototype.send = function (data) { + console.log("Sent:", data); + return wsSend(this, arguments); + }; +})(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 86ead53..0aae065 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,5 @@ "exactOptionalPropertyTypes": true, }, "exclude": ["node_modules"], - "include": ["src/*.ts", "src/util/extensionsettings.ts", "src/csfloat/content_script.ts", "src/eventhandler.ts"] + "include": ["src/*.ts", "src/**/*.ts"] } diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index 686d504..733512d 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -10,9 +10,11 @@ module.exports = { csfloat_script: { import: path.join(srcDir, 'csfloat/content_script.ts'), filename: 'csfloat/content_script.js' }, inject: path.join(srcDir, 'inject.ts'), injectionhandler: path.join(srcDir, 'injectionhandler.ts'), - // eventhandler: path.join(srcDir, 'eventhandler.ts'), - // mappinghandler: path.join(srcDir, 'mappinghandler.ts'), + eventhandler: path.join(srcDir, 'eventhandler.ts'), + mappinghandler: path.join(srcDir, 'mappinghandler.ts'), skinport_content: { import: path.join(srcDir, 'skinport/content_script.ts'), filename: 'skinport/content_script.js' }, + helperfunctions: { import: path.join(srcDir, 'util/helperfunctions.ts'), filename: 'util/helperfunctions.js' }, + extensionsettings: { import: path.join(srcDir, 'util/extensionsettings.ts'), filename: 'util/extensionsettings.js' }, }, output: { path: path.join(__dirname, "../dist/js"), From 3658e8aa178056ed7f411a0648fd27e785b0a00e Mon Sep 17 00:00:00 2001 From: GODrums Date: Mon, 28 Aug 2023 23:53:23 +0200 Subject: [PATCH 5/7] Skinport stuff main functionality done --- css/{stylesheet.css => csfloat_styles.css} | 1 + css/popup.css | 1 + css/skinport_styles.css | 32 ++++++ html/changelog.html | 13 +++ html/{settings.html => csfloat.html} | 0 html/index.html | 8 +- html/skinport.html | 52 ++++++++++ manifest.json | 3 +- src/@typings/FloatTypes.d.ts | 29 ++++++ src/background.ts | 5 + src/csfloat/content_script.ts | 9 +- src/eventhandler.ts | 6 +- src/mappinghandler.ts | 58 ++++++++--- src/popup.ts | 57 +++++++++-- src/skinport/content_script.ts | 108 ++++++++++++--------- src/util/extensionsettings.ts | 18 ++++ src/util/helperfunctions.ts | 15 +++ 17 files changed, 345 insertions(+), 70 deletions(-) rename css/{stylesheet.css => csfloat_styles.css} (96%) create mode 100644 css/skinport_styles.css rename html/{settings.html => csfloat.html} (100%) create mode 100644 html/skinport.html diff --git a/css/stylesheet.css b/css/csfloat_styles.css similarity index 96% rename from css/stylesheet.css rename to css/csfloat_styles.css index 5d166ef..eabd8de 100644 --- a/css/stylesheet.css +++ b/css/csfloat_styles.css @@ -47,6 +47,7 @@ .betterfloat-buffprice:hover .betterfloat-buff-tooltip { visibility: visible; opacity: 1; + transition: visibility 0s, opacity 0.5s linear; } .betterfloat-buffprice { diff --git a/css/popup.css b/css/popup.css index 3f643ff..e14c187 100644 --- a/css/popup.css +++ b/css/popup.css @@ -228,4 +228,5 @@ input[type='checkbox'] { color: rgba(255, 255, 255, 0.7); margin: 5px 20px 5px 0px; font-weight: 400; + overflow-wrap: break-word; } \ No newline at end of file diff --git a/css/skinport_styles.css b/css/skinport_styles.css new file mode 100644 index 0000000..16c0f59 --- /dev/null +++ b/css/skinport_styles.css @@ -0,0 +1,32 @@ +.betterfloat-buff-tooltip { + /* Positioning the tooltip text */ + position: absolute; + z-index: 1; + transform: translate(-16%, 40%); + visibility: hidden; + width: 200px; + background-color: #192733; + color: #fff; + text-align: center; + padding: 8px 10px 8px 10px; + border-radius: 6px; + font-size: 14px; + + /* Fade in tooltip */ + opacity: 0; + transition: opacity 0.3s; +} + +.betterfloat-buffprice { + font-size: 12px; +} + +.betterfloat-buffprice:hover .betterfloat-buff-tooltip { + visibility: visible; + opacity: 1; +} + +.betterfloat-sale-tag { + font-size: 12px!important; + margin: 1px; +} \ No newline at end of file diff --git a/html/changelog.html b/html/changelog.html index 84a7469..5e074b0 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -1,3 +1,16 @@ +
+
1.4.0
+
+

+ What's new? +

+
    +
  • FEATURE: First BETA support for skinport.com. Only selected features work for now. Please refer to the corresponding extension settings tab.
  • +
  • PRICING: As Steaminventoryhelper changed their CORS policy, requests now have to be proxied through a server. The deployed source code of the server can be found here: https://gist.github.com/GODrums/5b2d24c17c136a1b37acd14b1089933c
  • +
  • PRICING: Skinport displays item prices in your local currency. To provide actual accurate exchange rates, the exchangerate.host API is in use. If you want to use Skinport's questionable rates instead, feel free to select it in the options popup. Skinport will always charge in your (local) account currency.
  • +
+
+
1.3.2
diff --git a/html/settings.html b/html/csfloat.html similarity index 100% rename from html/settings.html rename to html/csfloat.html diff --git a/html/index.html b/html/index.html index c02f7fb..7d308bc 100644 --- a/html/index.html +++ b/html/index.html @@ -24,9 +24,13 @@