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

feat: key-value cache, user ID display on profile

Index 72f7874a f1084ac4

Changed files
+190 -40
entrypoints
profile.content
utils
+2 -2
entrypoints/home.content.ts
···
'favoritedPlaces',
async () => await api.batch('public', 'places/', places),
300000,
-
true
);
if (placeData == "disabled") {
···
'bestFriends',
async () => await api.batch('public', 'users/', friends),
300000,
-
true
);
if (userData == "disabled") {
···
'favoritedPlaces',
async () => await api.batch('public', 'places/', places),
300000,
+
false
);
if (placeData == "disabled") {
···
'bestFriends',
async () => await api.batch('public', 'users/', friends),
300000,
+
false
);
if (userData == "disabled") {
+37
entrypoints/profile.content/index.ts
···
···
+
import { preferences } from "@/utils/storage";
+
import config from "@/utils/config.json";
+
import * as view from "./view";
+
import { pullKVCache } from "@/utils/utilities";
+
+
export default defineContentScript({
+
matches: ['*://polytoria.com/u/*', '*://polytoria.com/users/*'],
+
main() {
+
preferences.getPreferences()
+
.then(async (values) => {
+
let userID;
+
const username = window.location.pathname.split('/')[2];
+
+
if (isNaN(username as any)) {
+
userID = await pullKVCache(
+
'userIDs',
+
username,
+
async () => {
+
if (!config.api.enabled) return "disabled";
+
+
const data = (await (await fetch(config.api.urls.public + "users/find?username=" + username)).json());
+
return data.id;
+
},
+
600000,
+
false
+
);
+
} else {
+
userID = username; // * Fallback for legacy profile URLs
+
}
+
+
if (window.location.pathname.split('/')[1] == "u") {
+
// View
+
view.displayId(userID);
+
};
+
});
+
}
+
});
+43
entrypoints/profile.content/view.ts
···
···
+
import config from "@/utils/config.json";
+
+
export async function displayId(userId: number) {
+
const statsCard = document.getElementById('user-stats-card');
+
if (!statsCard) return; // ? Incase the user is blocked, which means the stats card won't be present
+
+
const row = document.createElement('div');
+
row.classList.add('mb-1');
+
row.innerHTML = `
+
<b>
+
<i class="fa fa-hashtag text-center d-inline-block" style="width:1.3em"></i>
+
Player ID
+
</b>
+
<span class="float-end">
+
${userId}
+
<a id="copy" href="#copy">
+
<i class="fad fa-copy" style="margin-left: 5px;"></i>
+
</a>
+
</span>
+
`;
+
+
const copyBtn = row.getElementsByTagName('a')[0];
+
copyBtn.addEventListener('click', () => {
+
navigator.clipboard.writeText(userId as unknown as string)
+
.then(() => {
+
const icon: HTMLElement = copyBtn.children[0] as HTMLElement;
+
copyBtn.classList.add('text-success');
+
icon.setAttribute('class', 'fa-duotone fa-circle-check');
+
icon.style.marginLeft = '3px';
+
+
setTimeout(() => {
+
copyBtn.classList.remove('text-success');
+
icon.setAttribute('class', 'fad fa-copy');
+
icon.style.marginLeft = '5px';
+
}, 1500);
+
})
+
.catch(() => {
+
alert('Failure to copy user ID to clipboard.');
+
});
+
});
+
+
statsCard.children[0].insertBefore(row, statsCard.querySelector('.mb-1:has(.fa-calendar)')!);
+
};
+2 -1
utils/storage.ts
···
fallback: {
favoritedPlaces: [],
bestFriends: [],
-
inventory: []
},
version: 1
});
···
fallback: {
favoritedPlaces: [],
bestFriends: [],
+
inventory: [],
+
userIDs: {}
},
version: 1
});
+3
utils/types.ts
···
export type userDetails = {
username: string,
userId: number,
···
favoritedPlaces: never[],
bestFriends: never[],
inventory: never[],
[key: string]: any;
};
···
+
import { avatarApiSchema } from "./api/types";
+
export type userDetails = {
username: string,
userId: number,
···
favoritedPlaces: never[],
bestFriends: never[],
inventory: never[],
+
userIDs: Record<string, number>,
[key: string]: any;
};
+103 -37
utils/utilities.ts
···
import config from "@/utils/config.json";
import { cache } from "./storage";
-
import { avatarApiSchema, cacheInterface } from "./types";
-
import * as currencyPackages from "./currencyPackages.json"
-
export async function pullCache(key: string, replenish: Function, expiry: number, forceReplenish: boolean) {
const cacheStorage: cacheInterface = await cache.getValue();
-
const metadata = await cache.getMeta() as { [key: string]: number; };
-
const overlap = Date.now() - metadata[key];
-
if (!cacheStorage[key] || forceReplenish || (overlap >= expiry)) {
-
console.info('[Poly+] "' + key + '" cache is stale replenishing...', timeAgo(overlap));
const replenishedCache = await replenish();
-
// Don't cache the response when the config file has APIs disabled
-
if (replenishedCache != "disabled") {
cacheStorage[key] = replenishedCache;
metadata[key] = Date.now();
-
-
cache.setValue(cacheStorage);
-
cache.setMeta(metadata);
} else {
return "disabled";
-
};
};
-
return cacheStorage[key];
-
};
export async function expireCache(key: string) {
console.info('[Poly+] Forcefully expiring "' + key + '" cache...');
-
const metadata = await cache.getMeta() as { [key: string]: number };
metadata[key] = 0;
cache.setMeta(metadata);
-
};
function timeAgo(overlap: number) {
const units = [
-
{ label: 'day', value: 24 * 60 * 60 * 1000 },
-
{ label: 'hour', value: 60 * 60 * 1000 },
-
{ label: 'min', value: 60 * 1000 },
-
{ label: 'sec', value: 1000 },
];
for (const { label, value } of units) {
const count = Math.floor(overlap / value);
if (count > 0) {
-
return `${count} ${label}${count > 1 ? 's' : ''} ago`;
}
overlap %= value;
}
-
return 'just now';
-
};
export async function getUserDetails() {
-
const profileLink: HTMLLinkElement = document.querySelector('.navbar a.text-reset[href^="/users/"]')!;
-
const brickBalance = document.getElementsByClassName('brickBalanceCount')[0];
if (!profileLink || !brickBalance) return null;
-
const userId = parseInt(profileLink.href.split('/')[4]);
return {
username: profileLink.innerText.trim(),
userId: userId,
bricks: parseInt(brickBalance.textContent!.replace(/,/g, "")),
getAvatar: async () => {
if (config.api.enabled) {
-
const avatar = (await (await fetch(config.api.urls.public + 'users/' + userId + '/avatar')).json());
return avatar as avatarApiSchema;
} else {
return "disabled";
-
};
-
}
-
}
-
};
-
export function bricksToCurrency(bricks: number, currency: string): string | null {
if (isNaN(bricks) || bricks == 0) return null;
-
const _currencyPackages = currencyPackages as Record<string, Array<Array<number>>>;
-
const packages = _currencyPackages[currency].toSorted((a, b) => b[1] - a[1]);
if (!packages) {
-
console.warn('[Poly+] Missing currency package data for selected currency!');
return null;
}
···
}
return `~${totalValue.toFixed(2)} ${currency}`;
-
};
···
import config from "@/utils/config.json";
import { cache } from "./storage";
+
import { cacheInterface } from "./types";
+
import { avatarApiSchema } from "./api/types";
+
import * as currencyPackages from "./currencyPackages.json";
+
export async function pullCache(
+
key: string,
+
replenish: Function,
+
expiry: number,
+
forceReplenish: boolean,
+
) {
const cacheStorage: cacheInterface = await cache.getValue();
+
const metadata = (await cache.getMeta()) as { [key: string]: number };
+
const overlap = Date.now() - (metadata[key] || 0);
+
if (
+
!cacheStorage[key] || !metadata[key] || forceReplenish ||
+
overlap >= expiry
+
) {
+
console.info(
+
`[Poly+] "${key}" cache is stale replenishing...`,
+
timeAgo(overlap),
+
);
const replenishedCache = await replenish();
+
// Don't cache the response when the config file has APIs disabled
+
if (replenishedCache !== "disabled") {
cacheStorage[key] = replenishedCache;
metadata[key] = Date.now();
+
await cache.setValue(cacheStorage);
+
await cache.setMeta(metadata);
} else {
return "disabled";
+
}
+
}
+
+
return cacheStorage[key];
+
}
+
+
export async function pullKVCache(
+
store: string,
+
key: string,
+
replenish: Function,
+
expiry: number,
+
forceReplenish: boolean,
+
) {
+
const cacheStorage: cacheInterface = await cache.getValue();
+
const metadata = (await cache.getMeta()) as {
+
[key: string]: Record<string, number>;
};
+
+
if (!cacheStorage[store]) cacheStorage[store] = {};
+
if (!metadata[store]) metadata[store] = {};
+
const overlap = Date.now() - (metadata[store][key] || 0);
+
+
if (!cacheStorage[store][key] || forceReplenish || overlap >= expiry) {
+
console.info(
+
`[Poly+] "${key}" KV cache is stale replenishing...`,
+
timeAgo(overlap),
+
);
+
+
const replenishedCache = await replenish();
+
+
// Don't cache the response when the config file has APIs disabled
+
if (replenishedCache !== "disabled") {
+
cacheStorage[store][key] = replenishedCache;
+
metadata[store][key] = Date.now();
+
await cache.setValue(cacheStorage);
+
await cache.setMeta(metadata);
+
} else {
+
return "disabled";
+
}
+
}
+
+
return cacheStorage[store][key];
+
}
export async function expireCache(key: string) {
console.info('[Poly+] Forcefully expiring "' + key + '" cache...');
+
const metadata = await cache.getMeta() as { [key: string]: number };
metadata[key] = 0;
cache.setMeta(metadata);
+
}
function timeAgo(overlap: number) {
const units = [
+
{ label: "day", value: 24 * 60 * 60 * 1000 },
+
{ label: "hour", value: 60 * 60 * 1000 },
+
{ label: "min", value: 60 * 1000 },
+
{ label: "sec", value: 1000 },
];
for (const { label, value } of units) {
const count = Math.floor(overlap / value);
if (count > 0) {
+
return `${count} ${label}${count > 1 ? "s" : ""} ago`;
}
overlap %= value;
}
+
return "just now";
+
}
export async function getUserDetails() {
+
const profileLink: HTMLLinkElement = document.querySelector(
+
'.navbar a.text-reset[href^="/users/"]',
+
)!;
+
const brickBalance =
+
document.getElementsByClassName("brickBalanceCount")[0];
if (!profileLink || !brickBalance) return null;
+
const userId = parseInt(profileLink.href.split("/")[4]);
return {
username: profileLink.innerText.trim(),
userId: userId,
bricks: parseInt(brickBalance.textContent!.replace(/,/g, "")),
getAvatar: async () => {
if (config.api.enabled) {
+
const avatar =
+
await (await fetch(
+
config.api.urls.public + "users/" + userId + "/avatar",
+
)).json();
return avatar as avatarApiSchema;
} else {
return "disabled";
+
}
+
},
+
};
+
}
+
export function bricksToCurrency(
+
bricks: number,
+
currency: string,
+
): string | null {
if (isNaN(bricks) || bricks == 0) return null;
+
const _currencyPackages = currencyPackages as Record<
+
string,
+
Array<Array<number>>
+
>;
+
const packages = _currencyPackages[currency].toSorted((a, b) =>
+
b[1] - a[1]
+
);
if (!packages) {
+
console.warn(
+
"[Poly+] Missing currency package data for selected currency!",
+
);
return null;
}
···
}
return `~${totalValue.toFixed(2)} ${currency}`;
+
}