A rewrite of Poly+, my quality-of-life browser extension for Polytoria. Built entirely fresh using the WXT extension framework, Typescript, and with added better overall code quality.
extension
at main 5.3 kB view raw
1import config from "@/utils/config.json"; 2import { cache } from "./storage"; 3import { cacheInterface } from "./types"; 4import { avatarApiSchema } from "./api/types"; 5import * as currencyPackages from "./currencyPackages.json"; 6import { getAPI } from "./api"; 7 8export async function pullCache( 9 key: string, 10 replenish: Function, 11 expiry: number, 12 forceReplenish: boolean, 13) { 14 const cacheStorage: cacheInterface = await cache.getValue(); 15 const metadata = (await cache.getMeta()) as { [key: string]: number }; 16 17 const overlap = Date.now() - (metadata[key] || 0); 18 const shouldReplenish = !cacheStorage[key] || 19 cacheStorage[key] == "disabled" || 20 !metadata[key] || 21 forceReplenish || 22 (expiry !== -1 && overlap >= expiry); 23 24 if (shouldReplenish) { 25 if (config.devBuild) { 26 console.info( 27 `[Poly+] "${key}" cache ${ 28 expiry === -1 ? "doesn't exist" : "is stale" 29 } replenishing...`, 30 expiry !== -1 ? timeAgo(overlap) : "", 31 ); 32 } 33 34 const replenishedCache = await replenish(); 35 // Don't cache the response when the config file has APIs disabled 36 if (replenishedCache !== "disabled") { 37 cacheStorage[key] = replenishedCache; 38 metadata[key] = Date.now(); 39 await cache.setValue(cacheStorage); 40 await cache.setMeta(metadata); 41 } else { 42 return "disabled"; 43 } 44 } 45 46 return cacheStorage[key]; 47} 48 49export async function pullKVCache( 50 store: string, 51 key: string, 52 replenish: Function, 53 expiry: number, 54 forceReplenish: boolean, 55) { 56 const cacheStorage: cacheInterface = await cache.getValue(); 57 const metadata = (await cache.getMeta()) as { 58 [key: string]: Record<string, number>; 59 }; 60 61 if (!cacheStorage[store]) cacheStorage[store] = {}; 62 if (!metadata[store]) metadata[store] = {}; 63 64 const overlap = Date.now() - (metadata[store][key] || 0); 65 const shouldReplenish = !cacheStorage[store][key] || 66 cacheStorage[store][key] == "disabled" || 67 forceReplenish || 68 (expiry !== -1 && overlap >= expiry); 69 70 if (shouldReplenish) { 71 if (config.devBuild) { 72 console.info( 73 `[Poly+] "${key}" KV cache ${ 74 expiry === -1 ? "doesn't exist" : "is stale" 75 } replenishing...`, 76 expiry !== -1 ? timeAgo(overlap) : "", 77 ); 78 } 79 80 const replenishedCache = await replenish(); 81 // Don't cache the response when the config file has APIs disabled 82 if (replenishedCache !== "disabled") { 83 cacheStorage[store][key] = replenishedCache; 84 metadata[store][key] = Date.now(); 85 await cache.setValue(cacheStorage); 86 await cache.setMeta(metadata); 87 } else { 88 return "disabled"; 89 } 90 } 91 92 return cacheStorage[store][key]; 93} 94 95export async function expireCache(key: string) { 96 console.info('[Poly+] Forcefully expiring "' + key + '" cache...'); 97 98 const metadata = await cache.getMeta() as { [key: string]: number }; 99 metadata[key] = 0; 100 cache.setMeta(metadata); 101} 102 103function timeAgo(overlap: number) { 104 const units = [ 105 { label: "day", value: 24 * 60 * 60 * 1000 }, 106 { label: "hour", value: 60 * 60 * 1000 }, 107 { label: "min", value: 60 * 1000 }, 108 { label: "sec", value: 1000 }, 109 ]; 110 111 for (const { label, value } of units) { 112 const count = Math.floor(overlap / value); 113 if (count > 0) { 114 return `${count} ${label}${count > 1 ? "s" : ""} ago`; 115 } 116 overlap %= value; 117 } 118 119 return "just now"; 120} 121 122export async function getUserDetails() { 123 const publicAPI = getAPI("public"); 124 125 const profileLink: HTMLLinkElement = document.querySelector( 126 '.navbar a.text-reset[href^="/users/"]', 127 )!; 128 const brickBalance = document.getElementsByClassName("brickBalanceCount")[0]; 129 130 if (!profileLink || !brickBalance) return null; 131 132 const userId = parseInt(profileLink.href.split("/")[4]); 133 return { 134 username: profileLink.innerText.trim(), 135 userId: userId, 136 bricks: parseInt(brickBalance.textContent!.replace(/,/g, "")), 137 getAvatar: async () => { 138 if (publicAPI.enabled) { 139 const avatar = await (await fetch( 140 publicAPI.url + "users/" + userId + "/avatar", 141 )).json(); 142 return avatar as avatarApiSchema; 143 } else { 144 return "disabled"; 145 } 146 }, 147 }; 148} 149 150export function bricksToCurrency( 151 bricks: number, 152 currency: string, 153): string | null { 154 if (isNaN(bricks) || bricks == 0) return null; 155 156 const _currencyPackages = currencyPackages as { 157 base: Array<number[]>; 158 fromUSD: Record<string, number>; 159 updatedAt: string; 160 }; 161 const packages = _currencyPackages.base.toSorted((a, b) => b[1] - a[1]); 162 163 let totalValue = 0; 164 for (const [currencyValue, bricksValue] of packages) { 165 while (bricks >= bricksValue) { 166 bricks -= bricksValue; 167 totalValue += currencyValue; 168 } 169 } 170 171 if (bricks > 0) { 172 const cheapestPackage = packages[packages.length - 1]; 173 const [currencyValue, bricksValue] = cheapestPackage; 174 const unitPrice = currencyValue / bricksValue; 175 totalValue += bricks * unitPrice; 176 } 177 178 if (currency != "USD") { 179 if (!_currencyPackages.fromUSD[currency]) { 180 console.warn( 181 `[Poly+] Missing conversion from USD to ${currency}`, 182 ); 183 return null; 184 } 185 186 totalValue *= _currencyPackages.fromUSD[currency]; 187 } 188 189 return `~${totalValue.toFixed(2)} ${currency}`; 190}