mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 09:11:06 +00:00
121 lines
3.5 KiB
TypeScript
121 lines
3.5 KiB
TypeScript
import localforage from "localforage";
|
|
import { cloudAuth } from "@/seqta/utils/CloudAuth";
|
|
|
|
const ACCOUNTS_BASE = "https://accounts.betterseqta.org";
|
|
|
|
const store = localforage.createInstance({
|
|
name: "cloud-pfp-store",
|
|
storeName: "cloudPfp",
|
|
});
|
|
|
|
function hashKey(userId: string) {
|
|
return `hash:${userId}`;
|
|
}
|
|
|
|
function blobKey(userId: string) {
|
|
return `blob:${userId}`;
|
|
}
|
|
|
|
export function isAccountsHostedPfpUrl(url: string): boolean {
|
|
if (!url.includes("/api/user/pfp/")) return false;
|
|
if (url.includes("/hist/")) return false;
|
|
return /\/api\/user\/pfp\/[^/?#]+/.test(url.split("?")[0]!);
|
|
}
|
|
|
|
export function pfpUrlWithHash(url: string, hash: string | null | undefined): string {
|
|
if (!url || !hash || !isAccountsHostedPfpUrl(url)) return url;
|
|
const base = url.split("?")[0]!;
|
|
return `${base}?v=${hash}`;
|
|
}
|
|
|
|
export function defaultAccountsPfpUrl(userId: string): string {
|
|
return `${ACCOUNTS_BASE}/api/user/pfp/${userId}`;
|
|
}
|
|
|
|
async function fetchServerHash(userId: string): Promise<string | null> {
|
|
const res = await fetch(`${ACCOUNTS_BASE}/api/user/pfp/${userId}/meta`);
|
|
if (!res.ok) return null;
|
|
const data = (await res.json()) as { pfpHash?: string | null };
|
|
return data.pfpHash ?? null;
|
|
}
|
|
|
|
async function clearLocal(userId: string): Promise<void> {
|
|
await store.removeItem(hashKey(userId));
|
|
await store.removeItem(blobKey(userId));
|
|
}
|
|
|
|
export async function clearCloudPfpCache(userId?: string): Promise<void> {
|
|
const id = userId ?? cloudAuth.state.user?.id;
|
|
if (!id) return;
|
|
await clearLocal(id);
|
|
}
|
|
|
|
export type ResolveCloudPfpResult = {
|
|
src: string;
|
|
fromCache: boolean;
|
|
};
|
|
|
|
/**
|
|
* Returns an object URL or direct URL for the cloud profile picture.
|
|
* Order: session hash match → local blob; else meta → download → store blob then hash.
|
|
*/
|
|
export async function resolveCloudPfp(
|
|
userId: string,
|
|
pfpUrl: string,
|
|
): Promise<ResolveCloudPfpResult | null> {
|
|
if (!isAccountsHostedPfpUrl(pfpUrl)) {
|
|
return { src: pfpUrl, fromCache: false };
|
|
}
|
|
|
|
const sessionHash = cloudAuth.state.user?.pfpHash ?? null;
|
|
const localHash = await store.getItem<string>(hashKey(userId));
|
|
const localBlob = await store.getItem<Blob>(blobKey(userId));
|
|
|
|
let serverHash = sessionHash;
|
|
|
|
const localMatches =
|
|
!!serverHash && serverHash === localHash && localBlob instanceof Blob;
|
|
if (localMatches) {
|
|
return { src: URL.createObjectURL(localBlob), fromCache: true };
|
|
}
|
|
|
|
if (!serverHash || serverHash !== localHash) {
|
|
serverHash = await fetchServerHash(userId);
|
|
}
|
|
|
|
if (!serverHash) {
|
|
await clearLocal(userId);
|
|
return null;
|
|
}
|
|
|
|
if (serverHash === localHash && localBlob instanceof Blob) {
|
|
return { src: URL.createObjectURL(localBlob), fromCache: true };
|
|
}
|
|
|
|
await clearLocal(userId);
|
|
|
|
const imageUrl = pfpUrlWithHash(pfpUrl, serverHash);
|
|
const headers: HeadersInit = {};
|
|
if (localHash) {
|
|
headers["If-None-Match"] = `"${localHash}"`;
|
|
}
|
|
const token = await cloudAuth.getStoredToken();
|
|
if (token && isAccountsHostedPfpUrl(pfpUrl)) {
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(imageUrl, { headers });
|
|
if (res.status === 304 && localBlob instanceof Blob) {
|
|
await store.setItem(hashKey(userId), serverHash);
|
|
return { src: URL.createObjectURL(localBlob), fromCache: true };
|
|
}
|
|
|
|
if (!res.ok) return null;
|
|
|
|
const blob = await res.blob();
|
|
await store.setItem(blobKey(userId), blob);
|
|
await store.setItem(hashKey(userId), serverHash);
|
|
|
|
return { src: URL.createObjectURL(blob), fromCache: false };
|
|
}
|