diff --git a/README.md b/README.md index be6f3936..05454c9e 100644 --- a/README.md +++ b/README.md @@ -9,24 +9,22 @@ Installing is the same as the Vencord devs laid out: [(Install)](https://docs.ve # Vencord +![](https://img.shields.io/github/package-json/v/Vendicated/Vencord?style=for-the-badge&logo=github&logoColor=d3869b&label=&color=1d2021&labelColor=282828) [![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Vee/cord&color=2185D0&logo=data:image/png;base64,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)](https://codeberg.org/Vee/cord) The cutest Discord client mod -| ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) | -| :--------------------------------------------------------------------------------------------------: | -| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) | +![](https://github.com/user-attachments/assets/3fac98c0-c411-4d2a-97a3-13b7da8687a2) ## Features -- Super easy to install (Download Installer, open, click install button, done) -- 100+ plugins built in: [See a list](https://vencord.dev/plugins) - - Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, PermissionsViewer, CustomCommands, ShowHiddenChannels, PronounDB +- Easy to install +- [100+ built in plugins](https://vencord.dev/plugins) - Fairly lightweight despite the many inbuilt plugins - Excellent Browser Support: Run Vencord in your Browser via extension or UserScript -- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!) +- Works on any Discord branch: Stable, Canary or PTB all work - Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes) -- Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry +- Privacy friendly: blocks Discord analytics & crash reporting out of the box and has no telemetry - Maintained very actively, broken plugins are usually fixed within 12 hours - Settings sync: Keep your plugins and their settings synchronised between devices / apps (optional) diff --git a/browser/VencordNativeStub.ts b/browser/VencordNativeStub.ts index 79f0f2cd..c99c176c 100644 --- a/browser/VencordNativeStub.ts +++ b/browser/VencordNativeStub.ts @@ -20,16 +20,13 @@ /// import monacoHtmlLocal from "file://monacoWin.html?minify"; -import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify"; import * as DataStore from "../src/api/DataStore"; -import { debounce } from "../src/utils"; +import { debounce, localStorage } from "../src/utils"; import { EXTENSION_BASE_URL } from "../src/utils/web-metadata"; import { getTheme, Theme } from "../src/utils/discord"; import { getThemeInfo } from "../src/main/themes"; import { Settings } from "../src/Vencord"; - -// Discord deletes this so need to store in variable -const { localStorage } = window; +import { getStylusWebStoreUrl } from "@utils/web"; // listeners for ipc.on const cssListeners = new Set<(css: string) => void>(); @@ -45,12 +42,13 @@ window.VencordNative = { themes: { uploadTheme: (fileName: string, fileData: string) => DataStore.set(fileName, fileData, themeStore), deleteTheme: (fileName: string) => DataStore.del(fileName, themeStore), - getThemesDir: async () => "", getThemesList: () => DataStore.entries(themeStore).then(entries => entries.map(([name, css]) => getThemeInfo(css, name.toString())) ), getThemeData: (fileName: string) => DataStore.get(fileName, themeStore), getSystemValues: async () => ({}), + + openFolder: async () => Promise.reject("themes:openFolder is not supported on web"), }, native: { @@ -77,6 +75,14 @@ window.VencordNative = { addThemeChangeListener: NOOP, openFile: NOOP_ASYNC, async openEditor() { + if (IS_USERSCRIPT) { + const shouldOpenWebStore = confirm("QuickCSS is not supported on the Userscript. You can instead use the Stylus extension.\n\nDo you want to open the Stylus web store page?"); + if (shouldOpenWebStore) { + window.open(getStylusWebStoreUrl(), "_blank"); + } + return; + } + const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`; const win = open("about:blank", "VencordQuickCss", features); if (!win) { @@ -92,7 +98,7 @@ window.VencordNative = { ? "vs-light" : "vs-dark"; - win.document.write(IS_EXTENSION ? monacoHtmlLocal : monacoHtmlCdn); + win.document.write(monacoHtmlLocal); }, }, @@ -106,8 +112,9 @@ window.VencordNative = { } }, set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)), - getSettingsDir: async () => "LocalStorage" + openFolder: async () => Promise.reject("settings:openFolder is not supported on web"), }, pluginHelpers: {} as any, + csp: {} as any, }; diff --git a/package.json b/package.json index 4c6d5023..8a6173ef 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.12.2", + "version": "1.12.5", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index 7d21cd24..0ee24a31 100755 --- a/scripts/build/build.mjs +++ b/scripts/build/build.mjs @@ -31,6 +31,7 @@ const defines = stringifyValues({ IS_UPDATER_DISABLED, IS_WEB: false, IS_EXTENSION: false, + IS_USERSCRIPT: false, VERSION, BUILD_TIMESTAMP }); diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 824194cf..b22df8ab 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -43,6 +43,7 @@ const commonOptions = { define: stringifyValues({ IS_WEB: true, IS_EXTENSION: false, + IS_USERSCRIPT: false, IS_STANDALONE: true, IS_DEV, IS_REPORTER, @@ -98,6 +99,7 @@ const buildConfigs = [ inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])], define: { ...commonOptions.define, + IS_USERSCRIPT: "true", window: "unsafeWindow", }, outfile: "dist/Vencord.user.js", diff --git a/src/Vencord.ts b/src/Vencord.ts index 48ecce97..f18c7347 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -134,7 +134,11 @@ async function init() { if (!IS_WEB && !IS_UPDATER_DISABLED) { runUpdateCheck(); - setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes + + // this tends to get really annoying, so only do this if the user has auto-update without notification enabled + if (Settings.autoUpdate && !Settings.autoUpdateNotification) { + setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes + } } if (IS_DEV) { diff --git a/src/VencordNative.ts b/src/VencordNative.ts index 3bed5a59..048b30c7 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -5,6 +5,7 @@ */ import type { Settings } from "@api/Settings"; +import { CspRequestResult } from "@main/csp/manager"; import { PluginIpcMappings } from "@main/ipcPlugins"; import type { UserThemeHeader } from "@main/themes"; import { IpcEvents } from "@shared/IpcEvents"; @@ -33,10 +34,11 @@ export default { themes: { uploadTheme: (fileName: string, fileData: string) => invoke(IpcEvents.UPLOAD_THEME, fileName, fileData), deleteTheme: (fileName: string) => invoke(IpcEvents.DELETE_THEME, fileName), - getThemesDir: () => invoke(IpcEvents.GET_THEMES_DIR), getThemesList: () => invoke>(IpcEvents.GET_THEMES_LIST), getThemeData: (fileName: string) => invoke(IpcEvents.GET_THEME_DATA, fileName), getSystemValues: () => invoke>(IpcEvents.GET_THEME_SYSTEM_VALUES), + + openFolder: () => invoke(IpcEvents.OPEN_THEMES_FOLDER), }, updater: { @@ -49,7 +51,8 @@ export default { settings: { get: () => sendSync(IpcEvents.GET_SETTINGS), set: (settings: Settings, pathToNotify?: string) => invoke(IpcEvents.SET_SETTINGS, settings, pathToNotify), - getSettingsDir: () => invoke(IpcEvents.GET_SETTINGS_DIR), + + openFolder: () => invoke(IpcEvents.OPEN_SETTINGS_FOLDER), }, quickCss: { @@ -73,5 +76,17 @@ export default { openExternal: (url: string) => invoke(IpcEvents.OPEN_EXTERNAL, url) }, + csp: { + /** + * Note: Only supports full explicit matches, not wildcards. + * + * If `*.example.com` is allowed, `isDomainAllowed("https://sub.example.com")` will return false. + */ + isDomainAllowed: (url: string, directives: string[]) => invoke(IpcEvents.CSP_IS_DOMAIN_ALLOWED, url, directives), + removeOverride: (url: string) => invoke(IpcEvents.CSP_REMOVE_OVERRIDE, url), + requestAddOverride: (url: string, directives: string[], callerName: string) => + invoke(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, url, directives, callerName), + }, + pluginHelpers: PluginHelpers }; diff --git a/src/api/MessageAccessories.tsx b/src/api/MessageAccessories.tsx index 71664e93..d2bc081e 100644 --- a/src/api/MessageAccessories.tsx +++ b/src/api/MessageAccessories.tsx @@ -48,7 +48,7 @@ export function _modifyAccessories( ) { for (const [key, accessory] of accessories.entries()) { const res = ( - + ); diff --git a/src/api/Notifications/styles.css b/src/api/Notifications/styles.css index ad5c9cbc..10d3c0cf 100644 --- a/src/api/Notifications/styles.css +++ b/src/api/Notifications/styles.css @@ -3,8 +3,8 @@ all: unset; display: flex; flex-direction: column; - color: var(--text-normal); - background-color: var(--background-secondary-alt); + color: var(--text-default); + background-color: var(--background-base-lower-alt); border-radius: 6px; overflow: hidden; cursor: pointer; @@ -12,7 +12,7 @@ } .visual-refresh .vc-notification-root { - background-color: var(--bg-overlay-floating, var(--background-base-low)); + background-color: var(--background-base-low); } .vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) { diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 0ca20440..7058b5fd 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => { logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack); } + get isNoop() { + if (IS_DEV) return false; + return this.props.noop; + } + render() { if (this.state.error === NO_ERROR) return this.props.children; - if (this.props.noop) return null; + if (this.isNoop) return null; if (this.props.fallback) return ( diff --git a/src/components/ErrorCard.css b/src/components/ErrorCard.css index 5146aa03..2eea2f06 100644 --- a/src/components/ErrorCard.css +++ b/src/components/ErrorCard.css @@ -3,5 +3,9 @@ background-color: #e7828430; border: 1px solid #e78284; border-radius: 5px; - color: var(--text-normal, white); + color: var(--text-default, white); + + & a:hover { + text-decoration: underline; + } } diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 0f4eb07d..2eb7ab00 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -28,6 +28,9 @@ export function Link(props: React.PropsWithChildren) { props.style.pointerEvents = "none"; props["aria-disabled"] = true; } + + props.rel ??= "noreferrer"; + return ( {props.children} diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 7baeba08..17ab2662 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti {!!plugin.settingsAboutComponent && (
- + diff --git a/src/components/PluginSettings/styles.css b/src/components/PluginSettings/styles.css index a4f9aeee..ed5e9aa1 100644 --- a/src/components/PluginSettings/styles.css +++ b/src/components/PluginSettings/styles.css @@ -70,7 +70,7 @@ padding: 1em; background: var(--info-warning-background); border: 1px solid var(--info-warning-foreground); - color: var(--info-warning-text); + color: var(--info-warning-foreground); } .vc-plugins-restart-button { diff --git a/src/components/VencordSettings/CloudTab.tsx b/src/components/VencordSettings/CloudTab.tsx index a13c3f6c..809d4062 100644 --- a/src/components/VencordSettings/CloudTab.tsx +++ b/src/components/VencordSettings/CloudTab.tsx @@ -21,7 +21,7 @@ import { Settings, useSettings } from "@api/Settings"; import { CheckedTextInput } from "@components/CheckedTextInput"; import { Grid } from "@components/Grid"; import { Link } from "@components/Link"; -import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud"; +import { authorizeCloud, checkCloudUrlCsp, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud"; import { Margins } from "@utils/margins"; import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync"; import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common"; @@ -38,6 +38,8 @@ function validateUrl(url: string) { } async function eraseAllData() { + if (!await checkCloudUrlCsp()) return; + const res = await fetch(new URL("/v1/", getCloudUrl()), { method: "DELETE", headers: { Authorization: await getCloudAuth() } diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx index 55822069..accccd49 100644 --- a/src/components/VencordSettings/PatchHelperTab.tsx +++ b/src/components/VencordSettings/PatchHelperTab.tsx @@ -148,7 +148,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError )} {compileResult && - + {compileResult[1]} } diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index f718ab11..692e20bf 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -18,17 +18,21 @@ import { Settings, useSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; +import { ErrorCard } from "@components/ErrorCard"; import { Flex } from "@components/Flex"; import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons"; import { Link } from "@components/Link"; import { openPluginModal } from "@components/PluginSettings/PluginModal"; import type { UserThemeHeader } from "@main/themes"; +import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; -import { showItemInFolder } from "@utils/native"; -import { useAwaiter } from "@utils/react"; +import { classes } from "@utils/misc"; +import { relaunch } from "@utils/native"; +import { useAwaiter, useForceUpdater } from "@utils/react"; +import { getStylusWebStoreUrl } from "@utils/web"; import { findLazy } from "@webpack"; -import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; +import { Alerts, Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; import type { ComponentType, Ref, SyntheticEvent } from "react"; import Plugins from "~plugins"; @@ -65,7 +69,7 @@ function Validator({ link }: { link: string; }) { : "Valid!"; return {text}; } @@ -159,7 +163,6 @@ function ThemesTab() { const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL); const [themeText, setThemeText] = useState(settings.themeLinks.join("\n")); const [userThemes, setUserThemes] = useState(null); - const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir); useEffect(() => { refreshLocalThemes(); @@ -219,6 +222,12 @@ function ThemesTab() { If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder. + + External Resources + For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked. + Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts. + + <> @@ -241,8 +250,7 @@ function ThemesTab() { ) : ( showItemInFolder(themeDir!)} - disabled={themeDirPending} + action={() => VencordNative.themes.openFolder()} Icon={FolderIcon} /> )} @@ -347,10 +355,99 @@ function ThemesTab() { + {currentTab === ThemeTab.LOCAL && renderLocalThemes()} {currentTab === ThemeTab.ONLINE && renderOnlineThemes()} ); } -export default wrapTab(ThemesTab, "Themes"); +export function CspErrorCard() { + if (IS_WEB) return null; + + const errors = useCspErrors(); + const forceUpdate = useForceUpdater(); + + if (!errors.length) return null; + + const isImgurHtmlDomain = (url: string) => url.startsWith("https://imgur.com/"); + + const allowUrl = async (url: string) => { + const { origin: baseUrl, host } = new URL(url); + + const result = await VencordNative.csp.requestAddOverride(baseUrl, ["connect-src", "img-src", "style-src", "font-src"], "Vencord Themes"); + if (result !== "ok") return; + + CspBlockedUrls.forEach(url => { + if (new URL(url).host === host) { + CspBlockedUrls.delete(url); + } + }); + + forceUpdate(); + + Alerts.show({ + title: "Restart Required", + body: "A restart is required to apply this change", + confirmText: "Restart now", + cancelText: "Later!", + onConfirm: relaunch + }); + }; + + const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain); + + return ( + + Blocked Resources + Some images, styles, or fonts were blocked because they come from disallowed domains. + It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them. + + After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} to apply the change. + + + Blocked URLs +
+ {errors.map((url, i) => ( +
+ {i !== 0 && } +
+ {url} + +
+
+ ))} +
+ + {hasImgurHtmlDomain && ( + <> + + + Imgur links should be direct links in the form of https://i.imgur.com/... + + To obtain a direct link, right-click the image and select "Copy image address". + + )} +
+ ); +} + +function UserscriptThemesTab() { + return ( + + + Themes are not supported on the Userscript! + + + You can instead install themes with the Stylus extension! + + + + ); +} + +export default IS_USERSCRIPT + ? wrapTab(UserscriptThemesTab, "Themes") + : wrapTab(ThemesTab, "Themes"); diff --git a/src/components/VencordSettings/UpdaterTab.tsx b/src/components/VencordSettings/UpdaterTab.tsx index e29d7dfd..9871bfcc 100644 --- a/src/components/VencordSettings/UpdaterTab.tsx +++ b/src/components/VencordSettings/UpdaterTab.tsx @@ -94,7 +94,7 @@ function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof {message} - {author}
))} diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index 54da5a3f..047ba17d 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -26,8 +26,7 @@ import { gitRemote } from "@shared/vencordUserAgent"; import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants"; import { Margins } from "@utils/margins"; import { identity, isPluginDev } from "@utils/misc"; -import { relaunch, showItemInFolder } from "@utils/native"; -import { useAwaiter } from "@utils/react"; +import { relaunch } from "@utils/native"; import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common"; import BadgeAPI from "../../plugins/_api/badges"; @@ -53,9 +52,6 @@ type KeysOfType = { }[keyof Object]; function VencordSettings() { - const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, { - fallbackValue: "Loading..." - }); const settings = useSettings(); const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []); @@ -171,7 +167,7 @@ function VencordSettings() { showItemInFolder(settingsDir)} + action={() => VencordNative.settings.openFolder()} /> )} ; + +export const ConnectSrc = ["connect-src"]; +export const ImageSrc = [...ConnectSrc, "img-src"]; +export const CssSrc = ["style-src", "font-src"]; +export const ImageAndCssSrc = [...ImageSrc, ...CssSrc]; +export const ImageScriptsAndCssSrc = [...ImageAndCssSrc, "script-src", "worker-src"]; + +// Plugins can whitelist their own domains by importing this object in their native.ts +// script and just adding to it. But generally, you should just edit this file instead + +export const CspPolicies: PolicyMap = { + "http://localhost:*": ImageAndCssSrc, + "http://127.0.0.1:*": ImageAndCssSrc, + "localhost:*": ImageAndCssSrc, + "127.0.0.1:*": ImageAndCssSrc, + + "*.github.io": ImageAndCssSrc, // GitHub pages, used by most themes + "github.com": ImageAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes + "raw.githubusercontent.com": ImageAndCssSrc, // GitHub raw, used by some themes + "*.gitlab.io": ImageAndCssSrc, // GitLab pages, used by some themes + "gitlab.com": ImageAndCssSrc, // GitLab raw, used by some themes + "*.codeberg.page": ImageAndCssSrc, // Codeberg pages, used by some themes + "codeberg.org": ImageAndCssSrc, // Codeberg raw, used by some themes + + "*.githack.com": ImageAndCssSrc, // githack (namely raw.githack.com), used by some themes + "jsdelivr.net": ImageAndCssSrc, // jsDelivr, used by very few themes + + "fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes + + "i.imgur.com": ImageSrc, // Imgur, used by some themes + "i.ibb.co": ImageSrc, // ImgBB, used by some themes + "i.pinimg.com": ImageSrc, // Pinterest, used by some themes + "*.tenor.com": ImageSrc, // Tenor, used by some themes + "files.catbox.moe": ImageAndCssSrc, // Catbox, used by some themes + + "cdn.discordapp.com": ImageAndCssSrc, // Discord CDN, used by Vencord and some themes to load media + "media.discordapp.net": ImageSrc, // Discord media CDN, possible alternative to Discord CDN + + // CDNs used for some things by Vencord. + // FIXME: we really should not be using CDNs anymore + "cdnjs.cloudflare.com": ImageScriptsAndCssSrc, + "cdn.jsdelivr.net": ImageScriptsAndCssSrc, + + // Function Specific + "api.github.com": ConnectSrc, // used for updating Vencord itself + "ws.audioscrobbler.com": ConnectSrc, // Last.fm API + "translate-pa.googleapis.com": ConnectSrc, // Google Translate API + "*.vencord.dev": ImageSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev) + "manti.vendicated.dev": ImageSrc, // ReviewDB API + "decor.fieryflames.dev": ConnectSrc, // Decor API + "ugc.decor.fieryflames.dev": ImageSrc, // Decor CDN + "sponsor.ajay.app": ConnectSrc, // Dearrow API + "dearrow-thumb.ajay.app": ImageSrc, // Dearrow Thumbnail CDN + "usrbg.is-hardly.online": ImageSrc, // USRBG API + "icons.duckduckgo.com": ImageSrc, // DuckDuckGo Favicon API (Reverse Image Search) +}; + +const findHeader = (headers: PolicyMap, headerName: Lowercase) => { + return Object.keys(headers).find(h => h.toLowerCase() === headerName); +}; + +const parsePolicy = (policy: string): PolicyMap => { + const result: PolicyMap = {}; + policy.split(";").forEach(directive => { + const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g); + if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) { + result[directiveKey] = directiveValue; + } + }); + + return result; +}; + +const stringifyPolicy = (policy: PolicyMap): string => + Object.entries(policy) + .filter(([, values]) => values?.length) + .map(directive => directive.flat().join(" ")) + .join("; "); + + +const patchCsp = (headers: PolicyMap) => { + const reportOnlyHeader = findHeader(headers, "content-security-policy-report-only"); + if (reportOnlyHeader) + delete headers[reportOnlyHeader]; + + const header = findHeader(headers, "content-security-policy"); + + if (header) { + const csp = parsePolicy(headers[header][0]); + + const pushDirective = (directive: string, ...values: string[]) => { + csp[directive] ??= [...(csp["default-src"] ?? [])]; + csp[directive].push(...values); + }; + + pushDirective("style-src", "'unsafe-inline'"); + // we could make unsafe-inline safe by using strict-dynamic with a random nonce on our Vencord loader script https://content-security-policy.com/strict-dynamic/ + // HOWEVER, at the time of writing (24 Jan 2025), Discord is INSANE and also uses unsafe-inline + // Once they stop using it, we also should + pushDirective("script-src", "'unsafe-inline'", "'unsafe-eval'"); + + for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) { + pushDirective(directive, "blob:", "data:", "vencord:"); + } + + for (const [host, directives] of Object.entries(NativeSettings.store.customCspRules)) { + for (const directive of directives) { + pushDirective(directive, host); + } + } + + for (const [host, directives] of Object.entries(CspPolicies)) { + for (const directive of directives) { + pushDirective(directive, host); + } + } + + headers[header] = [stringifyPolicy(csp)]; + } +}; + +export function initCsp() { + session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => { + if (responseHeaders) { + if (resourceType === "mainFrame") + patchCsp(responseHeaders); + + // Fix hosts that don't properly set the css content type, such as + // raw.githubusercontent.com + if (resourceType === "stylesheet") { + const header = findHeader(responseHeaders, "content-type"); + if (header) + responseHeaders[header] = ["text/css"]; + } + } + + cb({ cancel: false, responseHeaders }); + }); + + // assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones. + // For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it + // impossible to load css from github raw despite our fix above + session.defaultSession.webRequest.onHeadersReceived = () => { }; +} diff --git a/src/main/csp/manager.ts b/src/main/csp/manager.ts new file mode 100644 index 00000000..e6b1a0e3 --- /dev/null +++ b/src/main/csp/manager.ts @@ -0,0 +1,125 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { NativeSettings } from "@main/settings"; +import { IpcEvents } from "@shared/IpcEvents"; +import { dialog, ipcMain, IpcMainInvokeEvent } from "electron"; + +import { CspPolicies, ImageAndCssSrc } from "."; + +export type CspRequestResult = "invalid" | "cancelled" | "unchecked" | "ok" | "conflict"; + +export function registerCspIpcHandlers() { + ipcMain.handle(IpcEvents.CSP_REMOVE_OVERRIDE, removeCspRule); + ipcMain.handle(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, addCspRule); + ipcMain.handle(IpcEvents.CSP_IS_DOMAIN_ALLOWED, isDomainAllowed); +} + +function validate(url: string, directives: string[]) { + try { + const { host } = new URL(url); + + if (/[;'"\\]/.test(host)) return false; + } catch { + return false; + } + + if (directives.length === 0) return false; + if (directives.some(d => !ImageAndCssSrc.includes(d))) return false; + + return true; +} + +function getMessage(url: string, directives: string[], callerName: string) { + const domain = new URL(url).host; + + const message = `${callerName} wants to allow connections to ${domain}`; + + let detail = + `Unless you recognise and fully trust ${domain}, you should cancel this request!\n\n` + + `You will have to fully close and restart ${IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} for the changes to take effect.`; + + if (directives.length === 1 && directives[0] === "connect-src") { + return { message, detail }; + } + + const contentTypes = directives + .filter(type => type !== "connect-src") + .map(type => { + switch (type) { + case "img-src": + return "Images"; + case "style-src": + return "CSS & Themes"; + case "font-src": + return "Fonts"; + default: + throw new Error(`Illegal CSP directive: ${type}`); + } + }) + .sort() + .join(", "); + + detail = `The following types of content will be allowed to load from ${domain}:\n${contentTypes}\n\n${detail}`; + + return { message, detail }; +} + +async function addCspRule(_: IpcMainInvokeEvent, url: string, directives: string[], callerName: string): Promise { + if (!validate(url, directives)) { + return "invalid"; + } + + const domain = new URL(url).host; + + if (domain in NativeSettings.store.customCspRules) { + return "conflict"; + } + + const { checkboxChecked, response } = await dialog.showMessageBox({ + ...getMessage(url, directives, callerName), + type: callerName ? "info" : "warning", + title: "Vencord Host Permissions", + buttons: ["Cancel", "Allow"], + defaultId: 0, + cancelId: 0, + checkboxLabel: `I fully trust ${domain} and understand the risks of allowing connections to it.`, + checkboxChecked: false, + }); + + if (response !== 1) { + return "cancelled"; + } + + if (!checkboxChecked) { + return "unchecked"; + } + + NativeSettings.store.customCspRules[domain] = directives; + return "ok"; +} + +function removeCspRule(_: IpcMainInvokeEvent, domain: string) { + if (domain in NativeSettings.store.customCspRules) { + delete NativeSettings.store.customCspRules[domain]; + return true; + } + + return false; +} + +function isDomainAllowed(_: IpcMainInvokeEvent, url: string, directives: string[]) { + try { + const domain = new URL(url).host; + + const ruleForDomain = CspPolicies[domain] ?? NativeSettings.store.customCspRules[domain]; + if (!ruleForDomain) return false; + + return directives.every(d => ruleForDomain.includes(d)); + } catch (e) { + return false; + } +} diff --git a/src/main/index.ts b/src/main/index.ts index 4cc2e0db..95301ff7 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -16,9 +16,11 @@ * along with this program. If not, see . */ -import { app, protocol, session } from "electron"; +import { app, net, protocol } from "electron"; import { join } from "path"; +import { pathToFileURL } from "url"; +import { initCsp } from "./csp"; import { ensureSafePath } from "./ipcMain"; import { RendererSettings } from "./settings"; import { IS_VANILLA, THEMES_DIR } from "./utils/constants"; @@ -26,21 +28,27 @@ import { installExt } from "./utils/extensions"; if (IS_VESKTOP || !IS_VANILLA) { app.whenReady().then(() => { - // Source Maps! Maybe there's a better way but since the renderer is executed - // from a string I don't think any other form of sourcemaps would work - protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => { - let url = unsafeUrl.slice("vencord://".length); + protocol.handle("vencord", ({ url: unsafeUrl }) => { + let url = decodeURI(unsafeUrl).slice("vencord://".length).replace(/\?v=\d+$/, ""); + if (url.endsWith("/")) url = url.slice(0, -1); + if (url.startsWith("/themes/")) { const theme = url.slice("/themes/".length); + const safeUrl = ensureSafePath(THEMES_DIR, theme); if (!safeUrl) { - cb({ statusCode: 403 }); - return; + return new Response(null, { + status: 404 + }); } - cb(safeUrl.replace(/\?v=\d+$/, "")); - return; + + return net.fetch(pathToFileURL(safeUrl).toString()); } + + // Source Maps! Maybe there's a better way but since the renderer is executed + // from a string I don't think any other form of sourcemaps would work + switch (url) { case "renderer.js.map": case "vencordDesktopRenderer.js.map": @@ -48,10 +56,11 @@ if (IS_VESKTOP || !IS_VANILLA) { case "vencordDesktopPreload.js.map": case "patcher.js.map": case "vencordDesktopMain.js.map": - cb(join(__dirname, url)); - break; + return net.fetch(pathToFileURL(join(__dirname, url)).toString()); default: - cb({ statusCode: 403 }); + return new Response(null, { + status: 404 + }); } }); @@ -63,70 +72,7 @@ if (IS_VESKTOP || !IS_VANILLA) { } catch { } - const findHeader = (headers: Record, headerName: Lowercase) => { - return Object.keys(headers).find(h => h.toLowerCase() === headerName); - }; - - // Remove CSP - type PolicyResult = Record; - - const parsePolicy = (policy: string): PolicyResult => { - const result: PolicyResult = {}; - policy.split(";").forEach(directive => { - const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g); - if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) { - result[directiveKey] = directiveValue; - } - }); - - return result; - }; - const stringifyPolicy = (policy: PolicyResult): string => - Object.entries(policy) - .filter(([, values]) => values?.length) - .map(directive => directive.flat().join(" ")) - .join("; "); - - const patchCsp = (headers: Record) => { - const header = findHeader(headers, "content-security-policy"); - - if (header) { - const csp = parsePolicy(headers[header][0]); - - for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) { - csp[directive] ??= []; - csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'"); - } - - // TODO: Restrict this to only imported packages with fixed version. - // Perhaps auto generate with esbuild - csp["script-src"] ??= []; - csp["script-src"].push("'unsafe-eval'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com"); - headers[header] = [stringifyPolicy(csp)]; - } - }; - - session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => { - if (responseHeaders) { - if (resourceType === "mainFrame") - patchCsp(responseHeaders); - - // Fix hosts that don't properly set the css content type, such as - // raw.githubusercontent.com - if (resourceType === "stylesheet") { - const header = findHeader(responseHeaders, "content-type"); - if (header) - responseHeaders[header] = ["text/css"]; - } - } - - cb({ cancel: false, responseHeaders }); - }); - - // assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones. - // For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it - // impossible to load css from github raw despite our fix above - session.defaultSession.webRequest.onHeadersReceived = () => { }; + initCsp(); }); } diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 62785867..6990cea9 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -28,14 +28,17 @@ import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs"; import { open, readdir, readFile } from "fs/promises"; import { join, normalize } from "path"; +import { registerCspIpcHandlers } from "./csp/manager"; import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes"; -import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants"; +import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants"; import { makeLinksOpenExternally } from "./utils/externalLinks"; mkdirSync(THEMES_DIR, { recursive: true }); +registerCspIpcHandlers(); + export function ensureSafePath(basePath: string, path: string) { - const normalizedBasePath = normalize(basePath); + const normalizedBasePath = normalize(basePath + "/"); const newPath = join(basePath, path); const normalizedPath = normalize(newPath); return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null; @@ -89,7 +92,6 @@ ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) => writeFileSync(QUICKCSS_PATH, css) ); -ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR); ipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes()); ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName)); ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({ @@ -97,6 +99,8 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({ "os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}` })); +ipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR)); +ipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR)); export function initIpc(mainWindow: BrowserWindow) { let quickCssWatcher: FSWatcher | undefined; diff --git a/src/main/settings.ts b/src/main/settings.ts index 3d367a94..962bbaa7 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -36,7 +36,6 @@ RendererSettings.addGlobalChangeListener(() => { } }); -ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR); ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain); ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => { @@ -49,16 +48,18 @@ export interface NativeSettings { [setting: string]: any; }; }; + customCspRules: Record; } const DefaultNativeSettings: NativeSettings = { - plugins: {} + plugins: {}, + customCspRules: {} }; const nativeSettings = readSettings("native", NATIVE_SETTINGS_FILE); mergeDefaults(nativeSettings, DefaultNativeSettings); -export const NativeSettings = new SettingsStore(nativeSettings); +export const NativeSettings = new SettingsStore(nativeSettings as NativeSettings); NativeSettings.addGlobalChangeListener(() => { try { diff --git a/src/main/updater/common.ts b/src/main/updater/common.ts index 41b9837c..1a0b8da9 100644 --- a/src/main/updater/common.ts +++ b/src/main/updater/common.ts @@ -35,7 +35,10 @@ export function serializeErrors(func: (...args: any[]) => any) { ok: false, error: e instanceof Error ? { // prototypes get lost, so turn error into plain object - ...e + ...e, + message: e.message, + name: e.name, + stack: e.stack } : e }; } diff --git a/src/main/updater/http.ts b/src/main/updater/http.ts index 9d42b5c6..a112dde3 100644 --- a/src/main/updater/http.ts +++ b/src/main/updater/http.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { get } from "@main/utils/simpleGet"; +import { fetchBuffer, fetchJson } from "@main/utils/http"; import { IpcEvents } from "@shared/IpcEvents"; import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent"; import { ipcMain } from "electron"; @@ -31,8 +31,8 @@ import { serializeErrors, VENCORD_FILES } from "./common"; const API_BASE = `https://api.github.com/repos/${gitRemote}`; let PendingUpdates = [] as [string, string][]; -async function githubGet(endpoint: string) { - return get(API_BASE + endpoint, { +async function githubGet(endpoint: string) { + return fetchJson(API_BASE + endpoint, { headers: { Accept: "application/vnd.github+json", // "All API requests MUST include a valid User-Agent header. @@ -46,9 +46,8 @@ async function calculateGitChanges() { const isOutdated = await fetchUpdates(); if (!isOutdated) return []; - const res = await githubGet(`/compare/${gitHash}...HEAD`); + const data = await githubGet(`/compare/${gitHash}...HEAD`); - const data = JSON.parse(res.toString("utf-8")); return data.commits.map((c: any) => ({ // github api only sends the long sha hash: c.sha.slice(0, 7), @@ -58,9 +57,8 @@ async function calculateGitChanges() { } async function fetchUpdates() { - const release = await githubGet("/releases/latest"); + const data = await githubGet("/releases/latest"); - const data = JSON.parse(release.toString()); const hash = data.name.slice(data.name.lastIndexOf(" ") + 1); if (hash === gitHash) return false; @@ -70,16 +68,20 @@ async function fetchUpdates() { PendingUpdates.push([name, browser_download_url]); } }); + return true; } async function applyUpdates() { - await Promise.all(PendingUpdates.map( - async ([name, data]) => writeFile( - join(__dirname, name), - await get(data) - ) - )); + const fileContents = await Promise.all(PendingUpdates.map(async ([name, url]) => { + const contents = await fetchBuffer(url); + return [join(__dirname, name), contents] as const; + })); + + await Promise.all(fileContents.map(async ([filename, contents]) => + writeFile(filename, contents)) + ); + PendingUpdates = []; return true; } diff --git a/src/main/utils/extensions.ts b/src/main/utils/extensions.ts index 1323bd37..1d7559fb 100644 --- a/src/main/utils/extensions.ts +++ b/src/main/utils/extensions.ts @@ -24,7 +24,7 @@ import { join } from "path"; import { DATA_DIR } from "./constants"; import { crxToZip } from "./crxToZip"; -import { get } from "./simpleGet"; +import { fetchBuffer } from "./http"; const extensionCacheDir = join(DATA_DIR, "ExtensionCache"); @@ -69,13 +69,14 @@ export async function installExt(id: string) { } catch (err) { const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`; - const buf = await get(url, { + const buf = await fetchBuffer(url, { headers: { "User-Agent": `Electron ${process.versions.electron} ~ Vencord (https://github.com/Vendicated/Vencord)` } }); - await extract(crxToZip(buf), extDir).catch(console.error); + await extract(crxToZip(buf), extDir) + .catch(err => console.error(`Failed to extract extension ${id}`, err)); } session.defaultSession.loadExtension(extDir); diff --git a/src/main/utils/http.ts b/src/main/utils/http.ts new file mode 100644 index 00000000..05dbca40 --- /dev/null +++ b/src/main/utils/http.ts @@ -0,0 +1,70 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { createWriteStream } from "original-fs"; +import { Readable } from "stream"; +import { finished } from "stream/promises"; + +type Url = string | URL; + +export async function checkedFetch(url: Url, options?: RequestInit) { + try { + var res = await fetch(url, options); + } catch (err) { + if (err instanceof Error && err.cause) { + err = err.cause; + } + + throw new Error(`${options?.method ?? "GET"} ${url} failed: ${err}`); + } + + if (res.ok) { + return res; + } + + let message = `${options?.method ?? "GET"} ${url}: ${res.status} ${res.statusText}`; + try { + const reason = await res.text(); + message += `\n${reason}`; + } catch { } + + throw new Error(message); +} + +export async function fetchJson(url: Url, options?: RequestInit) { + const res = await checkedFetch(url, options); + return res.json() as Promise; +} + +export async function fetchBuffer(url: Url, options?: RequestInit) { + const res = await checkedFetch(url, options); + const buf = await res.arrayBuffer(); + + return Buffer.from(buf); +} + +export async function downloadToFile(url: Url, path: string, options?: RequestInit) { + const res = await checkedFetch(url, options); + if (!res.body) { + throw new Error(`Download ${url}: response body is empty`); + } + + // @ts-expect-error weird type conflict + const body = Readable.fromWeb(res.body); + await finished(body.pipe(createWriteStream(path))); +} diff --git a/src/main/utils/simpleGet.ts b/src/main/utils/simpleGet.ts deleted file mode 100644 index 1a8302c0..00000000 --- a/src/main/utils/simpleGet.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import https from "https"; - -export function get(url: string, options: https.RequestOptions = {}) { - return new Promise((resolve, reject) => { - https.get(url, options, res => { - const { statusCode, statusMessage, headers } = res; - if (statusCode! >= 400) - return void reject(`${statusCode}: ${statusMessage} - ${url}`); - if (statusCode! >= 300) - return void resolve(get(headers.location!, options)); - - const chunks = [] as Buffer[]; - res.on("error", reject); - - res.on("data", chunk => chunks.push(chunk)); - res.once("end", () => resolve(Buffer.concat(chunks))); - }); - }); -} diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx index 8745584e..b00df6b0 100644 --- a/src/plugins/_api/badges/index.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -33,7 +33,7 @@ import definePlugin from "@utils/types"; import { Forms, Toasts, UserStore } from "@webpack/common"; import { User } from "discord-types/general"; -const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png"; +const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64"; const ContributorBadge: ProfileBadge = { description: "Vencord Contributor", diff --git a/src/plugins/_api/messagePopover.ts b/src/plugins/_api/messagePopover.ts index 3d55f62e..a297fc87 100644 --- a/src/plugins/_api/messagePopover.ts +++ b/src/plugins/_api/messagePopover.ts @@ -27,7 +27,7 @@ export default definePlugin({ { find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}", replacement: { - match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/, + match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?children:\[(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/, replace: (_, ReactButton, PotionButton, ButtonComponent, showReactButton, message) => "" + `]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,${showReactButton}&&${PotionButton},` } diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx index d24de222..8237be8b 100644 --- a/src/plugins/anonymiseFileNames/index.tsx +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -75,11 +75,7 @@ export default definePlugin({ find: "async uploadFiles(", replacement: [ { - match: /async uploadFiles\((\i),\i\){/, - replace: "$&$1.forEach($self.anonymise);" - }, - { - match: /async uploadFilesSimple\((\i)\){/, + match: /async uploadFiles\((\i)\){/, replace: "$&$1.forEach($self.anonymise);" } ], diff --git a/src/plugins/appleMusic.desktop/native.ts b/src/plugins/appleMusic.desktop/native.ts index 5a547997..c7dd40d2 100644 --- a/src/plugins/appleMusic.desktop/native.ts +++ b/src/plugins/appleMusic.desktop/native.ts @@ -27,7 +27,7 @@ interface RemoteData { let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null; const APPLE_MUSIC_BUNDLE_REGEX = /