* 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>
98 lines
3.3 KiB
TypeScript
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;
|
|
});
|