From 4b80b7353bf564c6e481c9a1ddce5f1e08e30ffa Mon Sep 17 00:00:00 2001 From: nex Date: Wed, 6 Mar 2024 11:18:00 +0000 Subject: [PATCH] Add current code --- server.py | 315 +++++++++++++++++++++++++++++++++++++++++++++ static/favicon.ico | Bin 0 -> 67646 bytes static/index.html | 35 +++++ static/script.js | 179 ++++++++++++++++++++++++++ static/style.css | 62 +++++++++ 5 files changed, 591 insertions(+) create mode 100755 server.py create mode 100755 static/favicon.ico create mode 100755 static/index.html create mode 100755 static/script.js create mode 100755 static/style.css diff --git a/server.py b/server.py new file mode 100755 index 0000000..27398e4 --- /dev/null +++ b/server.py @@ -0,0 +1,315 @@ +#!/bin/env python3 +# This code is licensed under GNU GPLv3. +import fastapi +import os +import time +import httpx +import typing +import hashlib +import pydantic +from pathlib import Path +from fastapi.staticfiles import StaticFiles +from fastapi.responses import Response, JSONResponse +from fastapi.middleware.cors import CORSMiddleware + + +class AppIDList(pydantic.BaseModel): + app_ids: list[int] | int + + +app = fastapi.FastAPI( + title="How much does The Sims 4 cost?", + description="A simple API to get the price of The Sims 4 (and technically other games) on Steam. Source @ /server.py", + license_info={ + "name": "GNU AGPLv3", + "identifier": "AGPL-3.0" + }, + contact={ + "name": "[Matrix] @nex", + "url": "https://matrix.to/#/@nex:nexy7574.co.uk" + }, + version="1.2.0", +) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=False, + allow_methods=["GET", "HEAD", "POST"], + allow_headers=["*"] +) +app.state.session = httpx.AsyncClient(http2=False) +app.state.cache = {} +app.state.ratelimit = {} +app.state.etags = {} + + +@app.post( + "/api/appinfo", + responses={ + 200: { + "description": "Successfully fetched all of the app info IDs.", + "headers": { + "X-Ratelimit-Count": { + "schema": { + "type": "integer" + }, + "description": "The number of requests you have sent to the API in the last 30 seconds." + }, + "X-Ratelimit-Remaining": { + "schema": { + "type": "integer" + }, + "description": "The number of requests you have left in the next 30 seconds." + }, + "X-Ratelimit-Reset": { + "schema": { + "type": "float" + }, + "description": "The unix timestamp of when the ratelimit resets." + }, + } + }, + 304: { + "description": "The ETag provided was valid and no data was returned.", + }, + 429: { + "description": "You hit the anti-abuse ratelimit. Check the `Retry-After` header for more information.", + "headers": { + "Retry-After": { + "schema": { + "type": "float" + }, + "description": "The amount of seconds you should wait before retrying." + } + } + }, + 500: { + "description": "There was an upstream error." + } + } +) +async def appinfo( + req: fastapi.Request, + body: AppIDList, + cc: str = fastapi.Query(None), + user_agent: str = fastapi.Header(..., alias="User-Agent"), + if_none_match: str = fastapi.Header(None, alias="If-None-Match"), +): + """ + This API endpoint proxies the call to the steam web API, and returns the data in a more usable format. + This also gets around the CORS issue that prevents this being a serverless solution. + + This endpoint: + - Caches the data internally for 24 hours, regardless of etag and cache control. + - Has a ratelimit of 10 requests per 20 seconds, shared across all endpoints. + """ + if req.client.host in app.state.ratelimit: + expires = app.state.ratelimit[req.client.host]["expires"] + count = app.state.ratelimit[req.client.host]["count"] + rl_remaining = 10 - count + now = time.time() + if expires >= now: + if count >= 10: + app.state.ratelimit[req.client.host]["expires"] += (2 * (1.125 ** count)) + return Response( + status_code=429, + headers={"Retry-After": str(app.state.ratelimit[req.client.host]["expires"] - now)} + ) + else: + app.state.ratelimit[req.client.host]["count"] += 1 + else: + del app.state.ratelimit[req.client.host] + else: + expires = time.time() + 20 + app.state.ratelimit[req.client.host] = { + "expires": expires, + "count": 1 + } + rl_remaining = 9 + count = 1 + + auto_cc = "N/A" + if if_none_match: + if_none_match = if_none_match.replace("W/", "").strip('"') + if not cc: + _ccr = await app.state.session.get(f"https://ipinfo.io/{req.client.host}/country") + cc = _ccr.text.strip().lower() + auto_cc = cc + app_ids = body.app_ids + if isinstance(app_ids, int): + app_ids = [app_ids] + + multi_fetch = len(app_ids) > 1 + cache_hit = "MISS" + + if if_none_match in app.state.etags: + if app.state.etags[if_none_match] > time.time(): + cache_hit = "FULL-HIT" + return Response(status_code=304) + else: + del app.state.etags[if_none_match] + + _md5 = hashlib.md5((",".join(map(str, app_ids)) + cc).encode(), usedforsecurity=False).hexdigest() + + values = {} + remaining = app_ids.copy() + for app_id in app_ids: + if int(app_id) in app.state.cache: + data, expires = app.state.cache[app_id] + if expires > time.time(): + cache_hit = "PARTIAL-HIT" + values[app_id] = data + remaining.pop(remaining.index(app_id)) + else: + del app.state.cache[app_id] + values[app_id] = None + else: + values[app_id] = None + + if len(remaining) == 0: + return JSONResponse( + values, + headers={ + "cache-control": "public, max-age=806400", + "X-Cache": "FULL-HIT", + "X-Ratelimit-Remaining": str(rl_remaining), + "X-Ratelimit-Count": str(count), + "X-Ratelimit-Reset": str(expires), + "X-Ratelimit-Reset-After": str(expires), + }, + ) + elif len(remaining) == len(app_ids): + cache_hit = "MISS" + + params = { + "appids": ",".join(map(str, remaining)) + } + if cc is not None: + params["cc"] = cc + + if multi_fetch: + params["filters"] = "price_overview" + + headers = { + "User-Agent": user_agent, + } + + result = original_response = await app.state.session.get( + "https://store.steampowered.com/api/appdetails", + params=params, + headers=headers, + ) + if result.status_code == 429: + return Response(status_code=429) + elif result.status_code not in [304, 200]: + raise fastapi.HTTPException(result.status_code, result.text) + else: + app.state.etag = result.headers.get("etag") + app.state.last_modified = result.headers.get("last-modified") + + result = result.json() + for _app_id, data in result.items(): + if "data" in data: + app.state.cache[int(_app_id)] = (data["data"], time.time() + 86400) + values[int(_app_id)] = data["data"] + + app.state.etags[_md5] = time.time() + 86400 + return JSONResponse( + values, + original_response.status_code, + headers={ + "ETag": _md5, + "cache-control": "public, max-age=806400", + "X-Cache": str(cache_hit), + "X-IP": str(req.client.host), + "X-Ratelimit-Remaining": str(rl_remaining), + "X-Ratelimit-Count": str(count), + "X-Ratelimit-Reset": str(expires), + "X-Ratelimit-Reset-After": str(expires), + }, + ) + + +RL_HEADERS = { + "X-Ratelimit-Throttled": { + "schema": { + "type": "boolean" + }, + "description": "Whether or not you are actively being throttled." + }, + "X-Ratelimit-Count": { + "schema": { + "type": "integer" + }, + "description": "The number of requests you have sent to the API in the last 30 seconds." + }, + "X-Ratelimit-Remaining": { + "schema": { + "type": "integer" + }, + "description": "The number of requests you have left in the next 30 seconds." + }, + "X-Ratelimit-Reset": { + "schema": { + "type": "float" + }, + "description": "The unix timestamp of when the ratelimit resets." + } +} +RL_DOC = { + 204: { + "description": "Successfully checked the ratelimit.", + "headers": RL_HEADERS + }, + 429: { + "description": "You hit the anti-abuse ratelimit. Check the `Retry-After` header for more information.", + "headers": { + **RL_HEADERS, + "Retry-After": { + "schema": { + "type": "float" + }, + "description": "The amount of seconds you should wait before retrying." + } + } + } +} + +@app.get("/api/ratelimit", status_code=204, responses=RL_DOC) +@app.head("/api/ratelimit", status_code=204, responses=RL_DOC) +async def check_ratelimit(req: fastapi.Request): + """ + Checks the current ratelimit. See GET /api/appinfo for more info. + """ + headers = { + "X-Ratelimit-Throttled": "False", + "X-Ratelimit-Remaining": "10", + "X-Ratelimit-Count": "0", + "X-Ratelimit-Reset": "0", + } + if req.client.host in app.state.ratelimit: + expires = app.state.ratelimit[req.client.host]["expires"] + count = app.state.ratelimit[req.client.host]["count"] + rl_remaining = 10 - count + + headers["X-Ratelimit-Remaining"] = str(rl_remaining) + headers["X-Ratelimit-Count"] = str(count) + headers["X-Ratelimit-Reset"] = str(expires) + headers["X-Ratelimit-Throttled"] = str(rl_remaining >= 10 and expires >= time.time()) + if headers["X-Ratelimit-Throttled"] == "True": + headers["Retry-After"] = str(expires - time.time()) + return Response(None, 204 if headers["X-Ratelimit-Throttled"] == "False" else 429, headers=headers) + + + +app.mount( + "/", + StaticFiles(directory=Path.cwd() / "static", html=True, check_dir=True, follow_symlink=True), +) + + +if __name__ == "__main__": + import uvicorn + import os + print("Starting in", Path.cwd()) + uvicorn.run(app, host="0.0.0.0", port=1037, forwarded_allow_ips="*") diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..314e8a941582b00def01424687defa711f0d7a35 GIT binary patch literal 67646 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 literal 0 HcmV?d00001 diff --git a/static/index.html b/static/index.html new file mode 100755 index 0000000..fa07dc7 --- /dev/null +++ b/static/index.html @@ -0,0 +1,35 @@ + + + + + + How much does the entire The Sims 4 game cost right now? + + + + + + + + + +
+

Currently The Sims 4 costs

+

£1000.00

+ + +
+ + + + diff --git a/static/script.js b/static/script.js new file mode 100755 index 0000000..69045ab --- /dev/null +++ b/static/script.js @@ -0,0 +1,179 @@ +// This code is licensed under GNU GPLv3. +const BASE_URL = "/api/appinfo"; +var TS4_APP_ID = 1222670; +const APPID_REGEX = /#(\d{5,10})/ +if(APPID_REGEX.test(window.location.hash)===true) { + let matches = APPID_REGEX.exec(window.location.hash); + TS4_APP_ID = parseInt(matches[1]) + console.debug("Set app ID to " + TS4_APP_ID); +} else { + console.debug("No custom ID set or does not match regex."); +} +const PRICE_ELEMENT = document.getElementById("price"); +const NAME_ELEMENT = document.getElementById("name"); + +/* + * These prices are gathered from a range of sources, such as Tesco, Amazon, and CeX. + * Some prices may no longer be correct. They do not include discounts. + * + * Last updated: 10/12/2023 + */ +const PRICES = { + "Freddo (18g)": 0.25, + "Skittles (136G)": 1.25, + "Cadbury Fingers (114G)": 1.70, + "howmuchdoesthesims4cost.lol": 1.78, + "Pepsi Max (2L)": 2.00, + "Pringles (Salt & Vinegar, 185g)": 2.25, + "BLÅHAJ Soft toy, shark (40 cm)": 22.0, + "Tenda AC10 V3.0 AC1200 Dual Band Gigabit Wireless Cable Router": 27.99, + "KIOXIA EXERIA NVMe M.2 SSD (1TB)": 43.20, + "Seagate BarraCuda,Internal Hard Drive 2.5 Inch (1TB)": 49.99, + "Kingston NV2 NVMe PCIe 4.0 Internal SSD (1TB)": 59.99, + "NVIDIA GeForce GTX 1650": 117.96, + "AMD Radeon RX 5700 8GB (Second hand from CeX)": 150.00, + "TP-Link AX5400 WiFi 6 Router": 159.99, + "Intel Core i5-13400F": 191.99, + "iPhone SE (3rd generation/5G/2022)": 499.00, + "My custom PC (https://uk.pcpartpicker.com/user/nexy7574/saved/#view=cKYV4D)": 800.0, + "NVIDIA GeForce RTX 4090": 1519.00, +} + +function compare(price, item) { + let n = Math.floor((price / PRICES[item]) * 100) / 100; + if(n>=1) { + n = Math.floor(n); + }; + return n +} + +function update(n, unit="$") { + PRICE_ELEMENT.textContent = unit + n; + console.debug("Updated price to: " + n); +} + + +function ids_to_body(ids) { + return JSON.stringify({"app_ids": ids}) +} + +function ratelimited(response) { + let retry_after = response.headers.get("retry-after"); + console.warn("Rate-limited for " + retry_after + " seconds."); + setTimeout(() => {window.location.reload()}, retry_after * 1000); + setInterval( + () => { + let element = document.getElementById("retry-after"); + let value = parseFloat(element.textContent); + value -= 0.1; + element.textContent = value.toFixed(1); + }, + 100 + ) + + let element = document.getElementById("retrying"); + element.hidden = false; + document.getElementById("retry-after").textContent = (retry_after * 1).toFixed(1); + document.querySelector("main").hidden = true; + document.querySelector("main").style.display = "none"; +} + + +async function get_app_info(id) { + const response = await fetch(BASE_URL, {method: "POST", body: ids_to_body([id]), headers: {"Content-Type": "application/json"}}); + if(response.status === 429) { + ratelimited(response); + return {id: {"name": ""}} + } + const data = await response.json(); + console.debug(data); + return data; +} + +async function calculate_price(ids) { + console.debug("Calculating price for: " + ids); + const response = await fetch(BASE_URL, {method: "POST", body: ids_to_body(ids), headers: {"Content-Type": "application/json"}}); + if(response.status === 429) { + ratelimited(response); + let o = {} + for (let id of ids) { + o[id] = {"price_overview": {"final": 0.0}} + } + return o + } + const data = await response.json(); + console.debug("mass price data:", data); + + let _price = 0.0; + let unit = "£"; + for (let value of Object.values(data)) { + console.debug("Value:", value) + if (value.price_overview == null) { + continue; + } + let new_unit = value.price_overview.final_formatted.slice(0, 1); + if((/\d/).test(new_unit)) { + //new_unit = value.price_overview.final_formatted.slice(-2, -1); + new_unit = value.price_overview.currency || '£' + } + unit = new_unit + _price += parseFloat(value.price_overview.final / 100); + } + return [_price, unit]; +} + +function find_unit(formatted) { + let unit = "£"; + if((/\d/).test(formatted.slice(0, 1))) { + unit = formatted.slice(-2, -1); + } + return unit; +} + +async function main() { + console.debug("Fetching app info for base game"); + let root; + try { + root = await get_app_info(TS4_APP_ID); + } catch (e) { + alert(`Error fetching information from Steam: ${e}`); + window.location.reload(); + }; + + console.debug("Fetching app info for DLCs"); + let key = Object.keys(root)[0]; + NAME_ELEMENT.textContent = root[key].name; + console.debug("Checking key: " + key); + let dlc_price = (root[key].price_overview || {final: 0.0}).final / 100; + let unit = find_unit((root[key].price_overview || {final_formatted: "£0.00"}).final_formatted); + if(root[key].dlc && root[key].dlc.length >= 0) { + document.getElementById("dlcs").hidden = false; + document.getElementById("dlc-count").textContent = root[key].dlc.length + " "; + let _result= await calculate_price(root[key].dlc) + dlc_price += _result[0]; + } + + if(root[key].header_image && root[key].capsule_image) { + let element2 = document.createElement("img"); + element2.src = root[key].header_image; + element2.alt = root[key].name; + element2.style.height = "2em" + NAME_ELEMENT.textContent = null; + NAME_ELEMENT.appendChild(element2); + } + + update(dlc_price.toFixed(2), unit); + + let element = document.getElementById("comparison"); + element.hidden = false; + for(let item of Object.keys(PRICES)) { + let price = compare(dlc_price, item); + if(price >= 0.95) { + let li = document.createElement("li"); + li.textContent = `${price}x ${item} (${unit}${PRICES[item]}/pc)`; + element.children[1].appendChild(li); + } + }; +} + +main(); diff --git a/static/style.css b/static/style.css new file mode 100755 index 0000000..009d66b --- /dev/null +++ b/static/style.css @@ -0,0 +1,62 @@ +:root { + --var-background: #fff; + --var-color: #000; + --var-background-inverted: #000 +} +@media (prefers-color-scheme: dark) { + :root { + --var-background: #000; + --var-color: #fff; + --var-background-inverted: #fff + } +} + +html, body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + background-color: var(--var-background); + color: var(--var-color); + text-align: center; + font-size: larger; + margin: 0; + padding: 0; + width: 100%; + height: 100%; +} + +@media screen and (max-width: 600px) { + html, body { + font-size: medium; + } +} + +h1, span img { + vertical-align: middle; +} + +#comparison { + text-align: left; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +#name { + background-size: cover; +} + +#retrying[hidden=false] { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + z-index: 100; + background: var(--var-background-inverted); + color: inverted(var(--var-color)); +}