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}