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