Equicord/src/equicordplugins/favouriteAnything/stores.ts
Davri 8f2cc6d52a
Rewrite favouriteAnything (#830)
* Add PendingReplyStore types

* Update uploadHandler and embedMedia types

* Update scroller types

* Update favouriteAnything

* Fix find

* Revert the devs array

* Move prop types to types.ts, separate AttachmentPreview lookups

* Update copyright

* Inline imports

* Fix crash

* Minor changes

* Replace useResizeObserver

* Add hostname checks to fetchAttachment

* Use managed style instead of direct import

* Update description

* Move ImageUtils to top

* change find

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
2026-02-25 08:47:42 -05:00

98 lines
3.3 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { proxyLazyWebpack } from "@webpack";
import { Constants, Flux, FluxDispatcher, RestAPI } from "@webpack/common";
import { RefreshedUrlsResponse } from "./types";
import { BatchedRequestQueue, isAllowedHost } from "./utils";
/** Used for storing and automatically refreshing signed CDN/Media proxy urls ({@link https://docs.discord.food/reference#signed-attachment-urls}). */
export const SignedUrlsStore = proxyLazyWebpack(() => {
interface Store {
get(url: string): string | null;
addSigned(url: string): void;
}
class SignedUrlsStore extends Flux.Store implements Store {
private static readonly _expirationThreshold = 60 * 60 * 1000;
private _urls = new Map<string, string>();
private _queue = new BatchedRequestQueue<string>(batch => this._handleBatch(batch), {
maxCount: 50,
timeout: 50
});
__getLocalVars() {
return { urls: this._urls, queue: this._queue };
}
public get(url: string): string | null {
const key = URL.parse(url);
if (!this._isValid(key)) return null;
const value = this._urls.get(`${this._clean(key)}`) ?? null;
const parsed = URL.parse(value!);
if (!parsed || this._willExpire(parsed)) this._refresh(key);
return value;
}
public addSigned(url: string): void {
const parsed = URL.parse(url);
if (!this._isValid(parsed)) return;
if (this._willExpire(parsed)) this._refresh(parsed);
else this._update([[`${this._clean(parsed)}`, url]]);
}
private _refresh(url: URL): void {
this._queue.add(`${this._clean(url)}`);
}
private _clean(url: URL): URL {
const clean = new URL(url);
clean.search = "";
clean.hash = "";
return clean;
}
private _isValid(url: URL | null): url is URL {
return !!(url && isAllowedHost(url.hostname));
}
private _willExpire(url: URL): boolean {
const expiryTimestamp = parseInt(url.searchParams.get("ex")!, 16) * 1000;
return isNaN(expiryTimestamp) || expiryTimestamp - SignedUrlsStore._expirationThreshold < Date.now();
}
private _update(urls: [string, string][]): void {
let hasChanged: boolean = false;
for (const [url, value] of urls) {
if (!value || url === value || this._urls.get(url) === value) continue;
this._urls.set(url, value);
hasChanged = true;
}
if (hasChanged) this.emitChange();
}
private async _handleBatch(batch: string[]): Promise<void> {
await RestAPI.post({
url: Constants.Endpoints.ATTACHMENTS_REFRESH_URLS,
body: { attachment_urls: batch },
retries: 3
}).then(({ body }: { body: RefreshedUrlsResponse; }) =>
this._update(body.refreshed_urls.map(({ original, refreshed }) => [original, refreshed!]))
);
}
}
return new SignedUrlsStore(FluxDispatcher) as Store;
});