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