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}