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