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}