FileUpload: use proxy more aggressively on Equibop (#917)

* fix(FileUpload): use proxy more aggressively on bop, also refactor s3 helper

* fix(FileUpload): handle copying to the clipboard if theres no text to copy

* feat(FileUpload): intercept discords file upload button

* me being mean

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
Key 2026-03-18 16:44:10 +00:00 • committed by GitHub
parent 3f9b44a346
commit 71f93f0848
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 201 additions and 58 deletions

View file

@ -0,0 +1,11 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const CORS_PROXY = "https://cors.keiran0.workers.dev";
export function toProxiedUrl(url: string): string {
return `${CORS_PROXY}?url=${encodeURIComponent(url)}`;
}

View file

@ -12,23 +12,75 @@ import { OpenExternalIcon } from "@components/Icons";
import { Devs, EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import definePlugin from "@utils/types";
import { Menu, PermissionsBits, PermissionStore, React, useEffect, useState } from "@webpack/common";
import { DraftType, FluxDispatcher, Menu, PermissionsBits, PermissionStore, React, useEffect, useState } from "@webpack/common";
import { settings } from "./settings";
import { serviceLabels, ServiceType } from "./types";
import { getMediaUrl } from "./utils/getMediaUrl";
import { cancelCurrentUpload, getUploadState, subscribeUploadState, uploadFile, uploadPickedFile } from "./utils/upload";
import { cancelCurrentUpload, getUploadState, isConfigured, subscribeUploadState, uploadFile, uploadPickedFile, uploadProvidedFiles } from "./utils/upload";
const cl = classNameFactory("vc-file-upload-");
let uploadAddFilesInterceptor: ((event: unknown) => void) | null = null;
type UploadAddFilesEvent = {
type: string;
files?: unknown;
uploads?: unknown;
items?: unknown;
draftType?: unknown;
};
function extractFilesFromValue(value: unknown): File[] {
if (value instanceof File) return [value];
if (!Array.isArray(value)) return [];
return value.flatMap(entry => {
if (entry instanceof File) return [entry];
if (!entry || typeof entry !== "object") return [];
const uploadFile = "file" in entry ? entry.file : null;
if (uploadFile instanceof File) return [uploadFile];
const item = "item" in entry && entry.item && typeof entry.item === "object" ? entry.item : null;
if (!item || !("file" in item)) return [];
return item.file instanceof File ? [item.file] : [];
});
}
function interceptUploadAddFiles(event: unknown): void {
if (!event || typeof event !== "object" || !("type" in event)) return;
const payload = event as UploadAddFilesEvent;
if (payload.type !== "UPLOAD_ATTACHMENT_ADD_FILES") return;
if (payload.draftType !== DraftType.ChannelMessage) return;
if (!Boolean((settings.store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload) || !isConfigured()) return;
const files = [
...extractFilesFromValue(payload.files),
...extractFilesFromValue(payload.uploads),
...extractFilesFromValue(payload.items)
];
const uniqueFiles = Array.from(new Set(files));
if (!uniqueFiles.length) return;
payload.files = [];
payload.uploads = [];
payload.items = [];
void uploadProvidedFiles(uniqueFiles);
}
const ProgressBarInner = () => {
const [state, setState] = useState(getUploadState);
useEffect(() => subscribeUploadState(() => setState(getUploadState())), []);
if (state.phase === "idle") {
return null;
}
if (state.phase === "idle") return null;
const percentage = Math.max(0, Math.min(100, state.percent));
@ -157,13 +209,44 @@ export default definePlugin({
match: /(textValue:.{0,50}channelId:\i\.id\}\))(?:,\i(,))?/,
replace: "$1,$self.renderUploadProgress()$2"
}
}
},
// forces an early return on the file size limit nitro upsell modal
{
find: "#{intl::tRuxk9::raw}",
replacement: {
match: /(?<=\.limits\.fileSize.{0,50})Array\.from\(\i\)\.some/,
replace: "$self.shouldBypassDiscordUploadSizeCheck()?false:$&"
}
},
],
contextMenus: {
"message": messageContextMenuPatch,
"image-context": imageContextMenuPatch,
"channel-attach": channelAttachMenuPatch
},
start() {
if (uploadAddFilesInterceptor) {
return;
}
uploadAddFilesInterceptor = event => interceptUploadAddFiles(event);
FluxDispatcher.addInterceptor(uploadAddFilesInterceptor);
},
stop() {
if (!uploadAddFilesInterceptor) {
return;
}
const index = FluxDispatcher._interceptors.indexOf(uploadAddFilesInterceptor);
if (index > -1) {
FluxDispatcher._interceptors.splice(index, 1);
}
uploadAddFilesInterceptor = null;
},
shouldBypassDiscordUploadSizeCheck(): boolean {
return Boolean((settings.store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload) && isConfigured();
},
renderUploadProgress() {
return <ProgressBar />;
}

View file

@ -166,6 +166,12 @@ export const settings = definePluginSettings({
default: false,
hidden: true
},
interceptDiscordUpload: {
type: OptionType.BOOLEAN,
description: "Intercept Discord uploads and use FileUpload instead.",
default: false,
hidden: true
},
gofileToken: {
type: OptionType.STRING,
description: "Optional GoFile API token",
@ -505,6 +511,13 @@ export function SettingsComponent() {
/>
</SettingsSection>
<SettingsSection tag="label" name="Intercept Discord Upload Button" description="Use FileUpload when uploading through Discord's file picker" inlineSetting>
<Switch
checked={Boolean((store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload)}
onChange={v => (store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload = v}
/>
</SettingsSection>
<SettingsSection name="Upload Timeout" description="Maximum time to wait per upload attempt before switching to fallback">
<Select
options={[

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { toProxiedUrl } from "@equicordplugins/fileUpload/constants";
import { settings } from "@equicordplugins/fileUpload/settings";
import { PluginNative } from "@utils/types";
@ -187,7 +188,7 @@ export async function uploadToS3(
throw new Error(result.error || "Upload failed");
}
} else {
const response = await fetch(uploadUrl.toString(), {
const response = await fetch(toProxiedUrl(uploadUrl.toString()), {
method: "PUT",
headers: requestHeaders,
body: fileBlob

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CORS_PROXY, toProxiedUrl } from "@equicordplugins/fileUpload/constants";
import { settings } from "@equicordplugins/fileUpload/settings";
import { serviceLabels, ServiceType, ShareXUploaderConfig, UploadResponse } from "@equicordplugins/fileUpload/types";
import { copyToClipboard } from "@utils/clipboard";
@ -24,7 +25,13 @@ const Native = IS_DISCORD_DESKTOP
const logger = new Logger("FileUpload", "#7cb7ff");
const CORS_PROXY = "https://cors.keiran0.workers.dev"; // im hosting this on cloudflare workers so uptime and latency should be reliable
function toProxyUrl(url: string): string {
if (Native || url.startsWith(`${CORS_PROXY}?url=`)) {
return url;
}
return toProxiedUrl(url);
}
let isUploading = false;
@ -59,6 +66,14 @@ const uploadStateListeners = new Set<() => void>();
let activeAbortController: AbortController | null = null;
let cancelRequested = false;
function isUploadCancelledError(error: unknown): boolean {
if (cancelRequested) return true;
if (!(error instanceof Error)) return false;
const message = error.message.toLowerCase();
return message.includes("cancelled") || message.includes("canceled") || message.includes("aborted") || message.includes("aborterror");
}
function emitUploadState() {
for (const listener of uploadStateListeners) {
listener();
@ -112,9 +127,10 @@ async function fetchWithTimeout(url: string, options: RequestInit): Promise<Resp
const controller = new AbortController();
activeAbortController = controller;
const timeout = setTimeout(() => controller.abort(), getUploadTimeoutMs());
const requestUrl = toProxyUrl(url);
try {
return await fetch(url, {
return await fetch(requestUrl, {
...options,
signal: controller.signal
});
@ -194,17 +210,7 @@ async function uploadToShareX(fileBlob: Blob, filename: string): Promise<string>
throw new Error(`Unsupported ShareX Body type: ${config.Body || "unknown"}`);
}
let response: Response;
try {
response = await fetchWithTimeout(requestUrl, { method, headers, body });
} catch (error) {
if (Native) {
throw error;
}
const proxiedUrl = `${CORS_PROXY}?url=${encodeURIComponent(requestUrl)}`;
response = await fetchWithTimeout(proxiedUrl, { method, headers, body });
}
const response = await fetchWithTimeout(requestUrl, { method, headers, body });
const responseText = await response.text();
let responseJson: unknown = null;
@ -302,9 +308,7 @@ async function uploadToNest(fileBlob: Blob, filename: string): Promise<string> {
const formData = new FormData();
formData.append("file", fileBlob, filename);
const proxiedUrl = `${CORS_PROXY}?url=${encodeURIComponent("https://nest.rip/api/files/upload")}`;
const response = await fetchWithTimeout(proxiedUrl, {
const response = await fetchWithTimeout("https://nest.rip/api/files/upload", {
method: "POST",
headers: {
"Authorization": nestToken
@ -394,8 +398,7 @@ async function uploadToEzHost(fileBlob: Blob, filename: string): Promise<string>
const headers: Record<string, string> = { key: ezHostKey };
const proxiedUrl = `${CORS_PROXY}?url=${encodeURIComponent("https://api.e-z.host/files")}`;
const response = await fetchWithTimeout(proxiedUrl, {
const response = await fetchWithTimeout("https://api.e-z.host/files", {
method: "POST",
headers,
body: formData
@ -428,7 +431,7 @@ async function uploadToCatbox(fileBlob: Blob, filename: string): Promise<string>
if (catboxUserhash) formData.append("userhash", catboxUserhash);
formData.append("fileToUpload", fileBlob, filename);
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent("https://catbox.moe/user/api.php")}`, {
const response = await fetchWithTimeout("https://catbox.moe/user/api.php", {
method: "POST",
body: formData
});
@ -472,7 +475,7 @@ async function uploadToLitterbox(fileBlob: Blob, filename: string): Promise<stri
formData.append("time", expiry);
formData.append("fileToUpload", fileBlob, filename);
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent("https://litterbox.catbox.moe/resources/internals/api.php")}`, {
const response = await fetchWithTimeout("https://litterbox.catbox.moe/resources/internals/api.php", {
method: "POST",
body: formData
});
@ -499,8 +502,7 @@ async function uploadToGofile(fileBlob: Blob, filename: string): Promise<string>
formData.append("file", fileBlob, filename);
const uploadUrl = "https://upload.gofile.io/uploadfile";
const requestUrl = Native ? uploadUrl : `${CORS_PROXY}?url=${encodeURIComponent(uploadUrl)}`;
const response = await fetchWithTimeout(requestUrl, {
const response = await fetchWithTimeout(uploadUrl, {
method: "POST",
body: formData
});
@ -535,7 +537,7 @@ async function uploadToTmpfiles(fileBlob: Blob, filename: string): Promise<strin
formData.append("file", fileBlob, filename);
const uploadUrl = "https://tmpfiles.org/api/v1/upload";
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent(uploadUrl)}`, {
const response = await fetchWithTimeout(uploadUrl, {
method: "POST",
body: formData
});
@ -563,7 +565,7 @@ async function uploadToBuzzheavier(fileBlob: Blob, filename: string): Promise<st
}
const uploadUrl = `https://w.buzzheavier.com/${encodeURIComponent(filename)}`;
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent(uploadUrl)}`, {
const response = await fetchWithTimeout(uploadUrl, {
method: "PUT",
body: fileBlob
});
@ -598,7 +600,7 @@ async function uploadToTempSh(fileBlob: Blob, filename: string): Promise<string>
formData.append("file", fileBlob, filename);
const uploadUrl = "https://temp.sh/upload";
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent(uploadUrl)}`, {
const response = await fetchWithTimeout(uploadUrl, {
method: "POST",
body: formData
});
@ -630,7 +632,7 @@ async function uploadToFilebin(fileBlob: Blob, filename: string): Promise<string
const formData = new FormData();
formData.append("file", fileBlob, filename);
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent(uploadUrl)}`, {
const response = await fetchWithTimeout(uploadUrl, {
method: "POST",
body: formData
});
@ -756,10 +758,20 @@ function finalizeUploadedUrl(url: string): string {
}
}
function notifyUploadSuccess(finalUrl: string): void {
async function notifyUploadSuccess(finalUrl: string): Promise<void> {
if (settings.store.autoCopy) {
copyToClipboard(finalUrl);
showToast("Upload successful, URL copied to clipboard", Toasts.Type.SUCCESS);
if (!finalUrl || !finalUrl.trim()) {
showToast("Upload successful, but no URL was available to copy", Toasts.Type.MESSAGE);
return;
}
try {
await copyToClipboard(finalUrl);
showToast("Upload successful, URL copied to clipboard", Toasts.Type.SUCCESS);
} catch (error) {
logger.warn("Upload succeeded but clipboard copy failed", error);
showToast("Upload successful, but failed to copy URL", Toasts.Type.MESSAGE);
}
} else {
showToast("Upload successful", Toasts.Type.SUCCESS);
}
@ -789,6 +801,8 @@ async function uploadWithFallbacks(fileBlob: Blob, filename: string, primary: Se
});
for (const service of uploadOrder) {
if (cancelRequested) throw new Error("Upload cancelled by user");
const attempt = attempted.length + 1;
setUploadState({
phase: attempt === 1 ? "uploading" : "retrying",
@ -819,11 +833,12 @@ async function uploadWithFallbacks(fileBlob: Blob, filename: string, primary: Se
return uploadedUrl;
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (message === "Upload cancelled by user") {
if (isUploadCancelledError(error)) {
throw error;
}
const message = error instanceof Error ? error.message : "Unknown error";
attempted.push(serviceLabels[service]);
lastError = message;
logger.warn(`Upload failed for ${serviceLabels[service]}: ${message}`);
@ -875,7 +890,7 @@ async function uploadPreparedBlob(blob: Blob, sourceUrl?: string): Promise<void>
setUploadState({ fileName: filename, status: "File ready, starting upload...", percent: 4 });
const uploadedUrl = await uploadWithFallbacks(normalizedBlob, filename, primary);
const finalUrl = finalizeUploadedUrl(uploadedUrl);
notifyUploadSuccess(finalUrl);
await notifyUploadSuccess(finalUrl);
}
export async function uploadFile(url: string): Promise<void> {
@ -926,7 +941,9 @@ export async function uploadFile(url: string): Promise<void> {
blob = await response.blob();
}
} else {
const response = await fetch(fetchUrl);
const response = await fetchWithTimeout(fetchUrl, {
method: "GET"
});
if (!response.ok) {
throw new Error(`Failed to fetch file: ${response.status}`);
}
@ -941,7 +958,7 @@ export async function uploadFile(url: string): Promise<void> {
await uploadPreparedBlob(blob, url);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (message === "Upload cancelled by user") {
if (isUploadCancelledError(error)) {
showToast("Upload cancelled", Toasts.Type.MESSAGE);
setUploadState({ phase: "cancelled", status: "Upload cancelled.", canCancel: false, percent: 0 });
} else {
@ -957,6 +974,13 @@ export async function uploadFile(url: string): Promise<void> {
}
export async function uploadPickedFile(): Promise<void> {
const file = await chooseFile("*/*");
if (!file) return;
await uploadProvidedFiles([file]);
}
export async function uploadProvidedFiles(files: readonly File[]): Promise<void> {
if (isUploading) {
showToast("Upload already in progress", Toasts.Type.MESSAGE);
return;
@ -967,30 +991,37 @@ export async function uploadPickedFile(): Promise<void> {
return;
}
const file = await chooseFile("*/*");
if (!file) {
return;
}
if (!files.length) return;
const uploadFiles = files.filter(file => Boolean(file));
if (!uploadFiles.length) return;
isUploading = true;
cancelRequested = false;
setUploadState({
phase: "preparing",
fileName: file.name,
currentService: null,
currentServiceLabel: "",
attempt: 0,
totalAttempts: 0,
percent: 2,
status: `Preparing ${file.name}...`,
canCancel: true
});
try {
await uploadPreparedBlob(file);
for (let i = 0; i < uploadFiles.length; i++) {
const file = uploadFiles[i];
const current = i + 1;
const suffix = uploadFiles.length > 1 ? ` (${current}/${uploadFiles.length})` : "";
setUploadState({
phase: "preparing",
fileName: file.name,
currentService: null,
currentServiceLabel: "",
attempt: 0,
totalAttempts: 0,
percent: 2,
status: `Preparing ${file.name}${suffix}...`,
canCancel: true
});
await uploadPreparedBlob(file);
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (message === "Upload cancelled by user") {
if (isUploadCancelledError(error)) {
showToast("Upload cancelled", Toasts.Type.MESSAGE);
setUploadState({ phase: "cancelled", status: "Upload cancelled.", canCancel: false, percent: 0 });
} else {

View file

@ -1272,6 +1272,10 @@ export const EquicordDevs = Object.freeze({
name: "m.shl",
id: 1025245410224263258n,
},
ScattrdBlade: {
name: "ScattrdBlade",
id: 678007540608532491n
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly