mirror of
https://github.com/Vendicated/Vencord.git
synced 2025-02-24 07:25:10 +00:00
Merge branch 'dev' into feat/api/SettingsLists
This commit is contained in:
commit
78d47a168c
36 changed files with 1350 additions and 507 deletions
66
.github/workflows/reportBrokenPlugins.yml
vendored
66
.github/workflows/reportBrokenPlugins.yml
vendored
|
@ -1,9 +1,22 @@
|
|||
name: Test Patches
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Every day at midnight
|
||||
- cron: 0 0 * * *
|
||||
inputs:
|
||||
discord_branch:
|
||||
type: choice
|
||||
description: "Discord Branch to test patches on"
|
||||
options:
|
||||
- both
|
||||
- stable
|
||||
- canary
|
||||
default: both
|
||||
webhook_url:
|
||||
type: string
|
||||
description: "Webhook URL that the report will be posted to. This will be visible for everyone, so DO NOT pass sensitive webhooks like discord webhook. This is meant to be used by Venbot."
|
||||
required: false
|
||||
# schedule:
|
||||
# # Every day at midnight
|
||||
# - cron: 0 0 * * *
|
||||
|
||||
jobs:
|
||||
TestPlugins:
|
||||
|
@ -40,28 +53,43 @@ jobs:
|
|||
- name: Build Vencord Reporter Version
|
||||
run: pnpm buildReporter
|
||||
|
||||
- name: Create Report
|
||||
- name: Run Reporter
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
|
||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||
env:
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
- name: Create Report (Canary)
|
||||
timeout-minutes: 10
|
||||
if: success() || failure() # even run if previous one failed
|
||||
run: |
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
export USE_CANARY=true
|
||||
stable_output_file=$(mktemp)
|
||||
canary_output_file=$(mktemp)
|
||||
|
||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||
pids=""
|
||||
|
||||
branch="${{ inputs.discord_branch }}"
|
||||
if [[ "${{ github.event_name }}" = "schedule" ]]; then
|
||||
branch="both"
|
||||
fi
|
||||
|
||||
if [[ "$branch" = "both" || "$branch" = "stable" ]]; then
|
||||
node dist/report.mjs > "$stable_output_file" &
|
||||
pids+=" $!"
|
||||
fi
|
||||
|
||||
if [[ "$branch" = "both" || "$branch" = "canary" ]]; then
|
||||
USE_CANARY=true node dist/report.mjs > "$canary_output_file" &
|
||||
pids+=" $!"
|
||||
fi
|
||||
|
||||
exit_code=0
|
||||
for pid in $pids; do
|
||||
if ! wait "$pid"; then
|
||||
exit_code=1
|
||||
fi
|
||||
done
|
||||
|
||||
cat "$stable_output_file" "$canary_output_file" >> $GITHUB_STEP_SUMMARY
|
||||
exit $exit_code
|
||||
env:
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
WEBHOOK_URL: ${{ inputs.webhook_url || secrets.DISCORD_WEBHOOK }}
|
||||
WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,6 +8,7 @@ vencord_installer
|
|||
.DS_Store
|
||||
|
||||
yarn.lock
|
||||
bun.lock
|
||||
package-lock.json
|
||||
|
||||
*.log
|
||||
|
|
|
@ -312,7 +312,7 @@ export const commonOpts = {
|
|||
logLevel: "info",
|
||||
bundle: true,
|
||||
watch,
|
||||
minify: !watch,
|
||||
minify: !watch && !IS_REPORTER,
|
||||
sourcemap: watch ? "inline" : "",
|
||||
legalComments: "linked",
|
||||
banner,
|
||||
|
|
|
@ -23,17 +23,24 @@
|
|||
// eslint-disable-next-line spaced-comment
|
||||
/// <reference types="../src/modules" />
|
||||
|
||||
import { createHmac } from "crypto";
|
||||
import { readFileSync } from "fs";
|
||||
import pup, { JSHandle } from "puppeteer-core";
|
||||
|
||||
for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
|
||||
const logStderr = (...data: any[]) => console.error(`${CANARY ? "CANARY" : "STABLE"} ---`, ...data);
|
||||
|
||||
for (const variable of ["CHROMIUM_BIN"]) {
|
||||
if (!process.env[variable]) {
|
||||
console.error(`Missing environment variable ${variable}`);
|
||||
logStderr(`Missing environment variable ${variable}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const CANARY = process.env.USE_CANARY === "true";
|
||||
let metaData = {
|
||||
buildNumber: "Unknown Build Number",
|
||||
buildHash: "Unknown Build Hash"
|
||||
};
|
||||
|
||||
const browser = await pup.launch({
|
||||
headless: true,
|
||||
|
@ -128,16 +135,18 @@ async function printReport() {
|
|||
|
||||
console.log();
|
||||
|
||||
if (process.env.DISCORD_WEBHOOK) {
|
||||
await fetch(process.env.DISCORD_WEBHOOK, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
description: "Here's the latest Vencord Report!",
|
||||
if (process.env.WEBHOOK_URL) {
|
||||
const body = JSON.stringify({
|
||||
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
|
||||
embeds: [
|
||||
{
|
||||
author: {
|
||||
name: `Discord ${CANARY ? "Canary" : "Stable"} (${metaData.buildNumber})`,
|
||||
url: `https://nelly.tools/builds/app/${metaData.buildHash}`,
|
||||
icon_url: CANARY ? "https://cdn.discordapp.com/emojis/1252721945699549327.png?size=128" : "https://cdn.discordapp.com/emojis/1252721943463985272.png?size=128"
|
||||
},
|
||||
color: CANARY ? 0xfbb642 : 0x5865f2
|
||||
},
|
||||
{
|
||||
title: "Bad Patches",
|
||||
description: report.badPatches.map(p => {
|
||||
|
@ -174,10 +183,26 @@ async function printReport() {
|
|||
color: report.otherErrors.length ? 0xff0000 : 0x00ff00
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
// functions similar to https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
|
||||
// used by venbot to ensure webhook invocations are genuine (since we will pass the webhook url as a workflow input which is publicly visible)
|
||||
// generate a secret with something like `openssl rand -hex 128`
|
||||
if (process.env.WEBHOOK_SECRET) {
|
||||
headers["X-Signature"] = "sha256=" + createHmac("sha256", process.env.WEBHOOK_SECRET).update(body).digest("hex");
|
||||
}
|
||||
|
||||
await fetch(process.env.WEBHOOK_URL, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body
|
||||
}).then(res => {
|
||||
if (!res.ok) console.error(`Webhook failed with status ${res.status}`);
|
||||
else console.error("Posted to Discord Webhook successfully");
|
||||
if (!res.ok) logStderr(`Webhook failed with status ${res.status}`);
|
||||
else logStderr("Posted to Webhook successfully");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -186,10 +211,13 @@ page.on("console", async e => {
|
|||
const level = e.type();
|
||||
const rawArgs = e.args();
|
||||
|
||||
async function getText() {
|
||||
async function getText(skipFirst = true) {
|
||||
let args = e.args();
|
||||
if (skipFirst) args = args.slice(1);
|
||||
|
||||
try {
|
||||
return await Promise.all(
|
||||
e.args().map(async a => {
|
||||
args.map(async a => {
|
||||
return await maybeGetError(a) || await a.jsonValue();
|
||||
})
|
||||
).then(a => a.join(" ").trim());
|
||||
|
@ -202,6 +230,12 @@ page.on("console", async e => {
|
|||
|
||||
const isVencord = firstArg === "[Vencord]";
|
||||
const isDebug = firstArg === "[PUP_DEBUG]";
|
||||
const isReporterMeta = firstArg === "[REPORTER_META]";
|
||||
|
||||
if (isReporterMeta) {
|
||||
metaData = await rawArgs[1].jsonValue() as any;
|
||||
return;
|
||||
}
|
||||
|
||||
outer:
|
||||
if (isVencord) {
|
||||
|
@ -215,10 +249,10 @@ page.on("console", async e => {
|
|||
|
||||
switch (tag) {
|
||||
case "WebpackInterceptor:":
|
||||
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
||||
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module|took [\d.]+?ms) \(Module id is (.+?)\): (.+)/)!;
|
||||
if (!patchFailMatch) break;
|
||||
|
||||
console.error(await getText());
|
||||
logStderr(await getText());
|
||||
process.exitCode = 1;
|
||||
|
||||
const [, plugin, type, id, regex] = patchFailMatch;
|
||||
|
@ -226,7 +260,7 @@ page.on("console", async e => {
|
|||
plugin,
|
||||
type,
|
||||
id,
|
||||
match: regex.replace(/\(\?:\[A-Za-z_\$\]\[\\w\$\]\*\)/g, "\\i"),
|
||||
match: regex,
|
||||
error: await maybeGetError(e.args()[3])
|
||||
});
|
||||
|
||||
|
@ -235,7 +269,7 @@ page.on("console", async e => {
|
|||
const failedToStartMatch = message.match(/Failed to start (.+)/);
|
||||
if (!failedToStartMatch) break;
|
||||
|
||||
console.error(await getText());
|
||||
logStderr(await getText());
|
||||
process.exitCode = 1;
|
||||
|
||||
const [, name] = failedToStartMatch;
|
||||
|
@ -246,7 +280,7 @@ page.on("console", async e => {
|
|||
|
||||
break;
|
||||
case "LazyChunkLoader:":
|
||||
console.error(await getText());
|
||||
logStderr(await getText());
|
||||
|
||||
switch (message) {
|
||||
case "A fatal error occurred:":
|
||||
|
@ -255,7 +289,7 @@ page.on("console", async e => {
|
|||
|
||||
break;
|
||||
case "Reporter:":
|
||||
console.error(await getText());
|
||||
logStderr(await getText());
|
||||
|
||||
switch (message) {
|
||||
case "A fatal error occurred:":
|
||||
|
@ -273,47 +307,36 @@ page.on("console", async e => {
|
|||
}
|
||||
|
||||
if (isDebug) {
|
||||
console.error(await getText());
|
||||
logStderr(await getText());
|
||||
} else if (level === "error") {
|
||||
const text = await getText();
|
||||
const text = await getText(false);
|
||||
|
||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
||||
if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {
|
||||
report.ignoredErrors.push(text);
|
||||
} else {
|
||||
console.error("[Unexpected Error]", text);
|
||||
logStderr("[Unexpected Error]", text);
|
||||
report.otherErrors.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
page.on("error", e => console.error("[Error]", e.message));
|
||||
page.on("error", e => logStderr("[Error]", e.message));
|
||||
page.on("pageerror", e => {
|
||||
if (e.message.includes("Sentry successfully disabled")) return;
|
||||
|
||||
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
|
||||
console.error("[Page Error]", e.message);
|
||||
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module") && !/^.{1,2}$/.test(e.message)) {
|
||||
logStderr("[Page Error]", e.message);
|
||||
report.otherErrors.push(e.message);
|
||||
} else {
|
||||
report.ignoredErrors.push(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
async function reporterRuntime(token: string) {
|
||||
Vencord.Webpack.waitFor(
|
||||
"loginToken",
|
||||
m => {
|
||||
console.log("[PUP_DEBUG]", "Logging in with token...");
|
||||
m.loginToken(token);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await page.evaluateOnNewDocument(`
|
||||
if (location.host.endsWith("discord.com")) {
|
||||
${readFileSync("./dist/browser.js", "utf-8")};
|
||||
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||
}
|
||||
`);
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ export * as Util from "./utils";
|
|||
export * as QuickCss from "./utils/quickCss";
|
||||
export * as Updater from "./utils/updater";
|
||||
export * as Webpack from "./webpack";
|
||||
export * as WebpackPatcher from "./webpack/patchWebpack";
|
||||
export { PlainSettings, Settings };
|
||||
|
||||
import "./utils/quickCss";
|
||||
|
|
|
@ -32,9 +32,10 @@ export interface Settings {
|
|||
autoUpdate: boolean;
|
||||
autoUpdateNotification: boolean,
|
||||
useQuickCss: boolean;
|
||||
eagerPatches: boolean;
|
||||
enabledThemes: string[];
|
||||
enableReactDevtools: boolean;
|
||||
themeLinks: string[];
|
||||
enabledThemes: string[];
|
||||
frameless: boolean;
|
||||
transparent: boolean;
|
||||
winCtrlQ: boolean;
|
||||
|
@ -81,6 +82,7 @@ const DefaultSettings: Settings = {
|
|||
autoUpdateNotification: true,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
eagerPatches: IS_REPORTER,
|
||||
enabledThemes: [],
|
||||
enableReactDevtools: false,
|
||||
frameless: false,
|
||||
|
|
|
@ -17,16 +17,22 @@
|
|||
*/
|
||||
|
||||
import { Button } from "@webpack/common";
|
||||
import { ButtonProps } from "@webpack/types";
|
||||
|
||||
import { Heart } from "./Heart";
|
||||
|
||||
export default function DonateButton(props: any) {
|
||||
export default function DonateButton({
|
||||
look = Button.Looks.LINK,
|
||||
color = Button.Colors.TRANSPARENT,
|
||||
...props
|
||||
}: Partial<ButtonProps>) {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
look={Button.Looks.LINK}
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={look}
|
||||
color={color}
|
||||
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")}
|
||||
innerClassName="vc-donate-button"
|
||||
>
|
||||
<Heart />
|
||||
Donate
|
||||
|
|
|
@ -16,14 +16,18 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function Heart() {
|
||||
import { classes } from "@utils/misc";
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export function Heart(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
height="16"
|
||||
width="16"
|
||||
style={{ marginRight: "0.5em", transform: "translateY(2px)" }}
|
||||
{...props}
|
||||
className={classes("vc-heart-icon", props.className)}
|
||||
>
|
||||
<path
|
||||
fill="#db61a2"
|
||||
|
|
77
src/components/VencordSettings/SpecialCard.tsx
Normal file
77
src/components/VencordSettings/SpecialCard.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./specialCard.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Card, Clickable, Forms, React } from "@webpack/common";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
const cl = classNameFactory("vc-special-");
|
||||
|
||||
interface StyledCardProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
description: string;
|
||||
cardImage?: string;
|
||||
backgroundImage?: string;
|
||||
backgroundColor?: string;
|
||||
buttonTitle?: string;
|
||||
buttonOnClick?: () => void;
|
||||
}
|
||||
|
||||
export function SpecialCard({ title, subtitle, description, cardImage, backgroundImage, backgroundColor, buttonTitle, buttonOnClick: onClick, children }: PropsWithChildren<StyledCardProps>) {
|
||||
const cardStyle: React.CSSProperties = {
|
||||
backgroundColor: backgroundColor || "#9c85ef",
|
||||
backgroundImage: `url(${backgroundImage || ""})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={cl("card", "card-special")} style={cardStyle}>
|
||||
<div className={cl("card-flex")}>
|
||||
<div className={cl("card-flex-main")}>
|
||||
<Forms.FormTitle className={cl("title")} tag="h5">{title}</Forms.FormTitle>
|
||||
<Forms.FormText className={cl("subtitle")}>{subtitle}</Forms.FormText>
|
||||
<Forms.FormText className={cl("text")}>{description}</Forms.FormText>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
{cardImage && (
|
||||
<div className={cl("image-container")}>
|
||||
<img
|
||||
role="presentation"
|
||||
src={cardImage}
|
||||
alt=""
|
||||
className={cl("image")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{buttonTitle && (
|
||||
<>
|
||||
<Forms.FormDivider className={cl("seperator")} />
|
||||
<Clickable onClick={onClick} className={cl("hyperlink")}>
|
||||
<Forms.FormText className={cl("hyperlink-text")}>
|
||||
{buttonTitle}
|
||||
</Forms.FormText>
|
||||
</Clickable>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -20,29 +20,38 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
|||
import { useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
|
||||
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||
import { gitRemote } from "@shared/vencordUserAgent";
|
||||
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity } from "@utils/misc";
|
||||
import { identity, isPluginDev } from "@utils/misc";
|
||||
import { relaunch, showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { Button, Card, Forms, React, Select, Switch } from "@webpack/common";
|
||||
import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
|
||||
|
||||
import BadgeAPI from "../../plugins/_api/badges";
|
||||
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
|
||||
import { openNotificationSettingsModal } from "./NotificationSettings";
|
||||
import { QuickAction, QuickActionCard } from "./quickActions";
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
import { SpecialCard } from "./SpecialCard";
|
||||
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
|
||||
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
|
||||
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
|
||||
|
||||
const VENNIE_DONATOR_IMAGE = "https://cdn.discordapp.com/emojis/1238120638020063377.png";
|
||||
const COZY_CONTRIB_IMAGE = "https://cdn.discordapp.com/emojis/1026533070955872337.png";
|
||||
|
||||
const DONOR_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070116305436712.png?size=2048";
|
||||
const CONTRIB_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070166481895484.png?size=2048";
|
||||
|
||||
type KeysOfType<Object, Type> = {
|
||||
[K in keyof Object]: Object[K] extends Type ? K : never;
|
||||
}[keyof Object];
|
||||
|
||||
|
||||
function VencordSettings() {
|
||||
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
|
||||
fallbackValue: "Loading..."
|
||||
|
@ -55,6 +64,8 @@ function VencordSettings() {
|
|||
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
||||
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
|
||||
|
||||
const user = UserStore.getCurrentUser();
|
||||
|
||||
const Switches: Array<false | {
|
||||
key: KeysOfType<typeof settings, boolean>;
|
||||
title: string;
|
||||
|
@ -99,7 +110,44 @@ function VencordSettings() {
|
|||
|
||||
return (
|
||||
<SettingsTab title="Vencord Settings">
|
||||
<DonateCard image={donateImage} />
|
||||
{isDonor(user?.id)
|
||||
? (
|
||||
<SpecialCard
|
||||
title="Donations"
|
||||
subtitle="Thank you for donating!"
|
||||
description="All Vencord users can see your badge! You can change it at any time by messaging @vending.machine."
|
||||
cardImage={VENNIE_DONATOR_IMAGE}
|
||||
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
||||
backgroundColor="#ED87A9"
|
||||
>
|
||||
<DonateButtonComponent />
|
||||
</SpecialCard>
|
||||
)
|
||||
: (
|
||||
<SpecialCard
|
||||
title="Support the Project"
|
||||
description="Please consider supporting the development of Vencord by donating!"
|
||||
cardImage={donateImage}
|
||||
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
||||
backgroundColor="#c3a3ce"
|
||||
>
|
||||
<DonateButtonComponent />
|
||||
</SpecialCard>
|
||||
)
|
||||
}
|
||||
{isPluginDev(user?.id) && (
|
||||
<SpecialCard
|
||||
title="Contributions"
|
||||
subtitle="Thank you for contributing!"
|
||||
description="Since you've contributed to Vencord you now have a cool new badge!"
|
||||
cardImage={COZY_CONTRIB_IMAGE}
|
||||
backgroundImage={CONTRIB_BACKGROUND_IMAGE}
|
||||
backgroundColor="#EDCC87"
|
||||
buttonTitle="See what you've contributed to"
|
||||
buttonOnClick={() => openContributorModal(user)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Forms.FormSection title="Quick Actions">
|
||||
<QuickActionCard>
|
||||
<QuickAction
|
||||
|
@ -239,31 +287,19 @@ function VencordSettings() {
|
|||
);
|
||||
}
|
||||
|
||||
interface DonateCardProps {
|
||||
image: string;
|
||||
}
|
||||
|
||||
function DonateCard({ image }: DonateCardProps) {
|
||||
function DonateButtonComponent() {
|
||||
return (
|
||||
<Card className={cl("card", "donate")}>
|
||||
<div>
|
||||
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
|
||||
<Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText>
|
||||
<DonateButton style={{ transform: "translateX(-1em)" }} />
|
||||
</div>
|
||||
<img
|
||||
role="presentation"
|
||||
src={image}
|
||||
alt=""
|
||||
height={128}
|
||||
style={{
|
||||
imageRendering: image === SHIGGY_DONATE_IMAGE ? "pixelated" : void 0,
|
||||
marginLeft: "auto",
|
||||
transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : void 0
|
||||
}}
|
||||
<DonateButton
|
||||
look={Button.Looks.FILLED}
|
||||
color={Button.Colors.WHITE}
|
||||
style={{ marginTop: "1em" }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function isDonor(userId: string): boolean {
|
||||
const donorBadges = BadgeAPI.getDonorBadges(userId);
|
||||
return GuildMemberStore.getMember(VENCORD_GUILD_ID, userId)?.roles.includes(DONOR_ROLE_ID) || !!donorBadges;
|
||||
}
|
||||
|
||||
export default wrapTab(VencordSettings, "Vencord Settings");
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
.vc-settings-quickActions-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, max-content));
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5em;
|
||||
justify-content: center;
|
||||
padding: 0.5em 0;
|
||||
padding: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@media (width <=1040px) {
|
||||
.vc-settings-quickActions-card {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-pill {
|
||||
all: unset;
|
||||
background: var(--background-secondary);
|
||||
|
@ -14,12 +19,16 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 8px 12px;
|
||||
border-radius: 9999px;
|
||||
padding: 8px 9px;
|
||||
border-radius: 8px;
|
||||
transition: 0.1s ease-out;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-pill:hover {
|
||||
background: var(--background-secondary-alt);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--elevation-high);
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-pill:focus-visible {
|
||||
|
|
92
src/components/VencordSettings/specialCard.css
Normal file
92
src/components/VencordSettings/specialCard.css
Normal file
|
@ -0,0 +1,92 @@
|
|||
.vc-donate-button {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.vc-donate-button .vc-heart-icon {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.vc-donate-button:hover .vc-heart-icon {
|
||||
transform: scale(1.1);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vc-settings-card {
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vc-special-card-special {
|
||||
padding: 1em 1.5em;
|
||||
margin-bottom: 1em;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.vc-special-card-flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.vc-special-card-flex-main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-special-title {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.vc-special-subtitle {
|
||||
color: black;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.vc-special-text {
|
||||
color: black;
|
||||
font-size: 1em;
|
||||
margin-top: .75em;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.vc-special-seperator {
|
||||
margin-top: .75em;
|
||||
border-top: 1px solid white;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.vc-special-hyperlink {
|
||||
margin-top: 1em;
|
||||
cursor: pointer;
|
||||
|
||||
.vc-special-hyperlink-text {
|
||||
color: black;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
transition: text-decoration 0.5s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover .vc-special-hyperlink-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.vc-special-image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: 1em;
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.vc-special-image {
|
||||
width: 65%;
|
||||
}
|
|
@ -5,3 +5,8 @@
|
|||
.vc-owner-crown-icon {
|
||||
color: var(--text-warning);
|
||||
}
|
||||
|
||||
.vc-heart-icon {
|
||||
margin-right: 0.5em;
|
||||
translate: 0 2px;
|
||||
}
|
||||
|
|
|
@ -23,35 +23,61 @@ if (IS_DEV || IS_REPORTER) {
|
|||
var logger = new Logger("Tracer", "#FFD166");
|
||||
}
|
||||
|
||||
const noop = function () { };
|
||||
|
||||
export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
|
||||
export const beginTrace = !(IS_DEV || IS_REPORTER) ? () => { } :
|
||||
function beginTrace(name: string, ...args: any[]) {
|
||||
if (name in traces)
|
||||
if (name in traces) {
|
||||
throw new Error(`Trace ${name} already exists!`);
|
||||
}
|
||||
|
||||
traces[name] = [performance.now(), args];
|
||||
};
|
||||
|
||||
export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
|
||||
export const finishTrace = !(IS_DEV || IS_REPORTER) ? () => 0 :
|
||||
function finishTrace(name: string) {
|
||||
const end = performance.now();
|
||||
|
||||
const [start, args] = traces[name];
|
||||
delete traces[name];
|
||||
|
||||
logger.debug(`${name} took ${end - start}ms`, args);
|
||||
const totalTime = end - start;
|
||||
logger.debug(`${name} took ${totalTime}ms`, args);
|
||||
|
||||
return totalTime;
|
||||
};
|
||||
|
||||
type Func = (...args: any[]) => any;
|
||||
type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
|
||||
|
||||
const noopTracer =
|
||||
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
|
||||
function noopTracerWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
|
||||
return function (this: unknown, ...args: Parameters<F>): [ReturnType<F>, number] {
|
||||
return [f.apply(this, args), 0];
|
||||
};
|
||||
}
|
||||
|
||||
function noopTracer<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
|
||||
return f;
|
||||
}
|
||||
|
||||
export const traceFunctionWithResults = !(IS_DEV || IS_REPORTER)
|
||||
? noopTracerWithResults
|
||||
: function traceFunctionWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): (this: unknown, ...args: Parameters<F>) => [ReturnType<F>, number] {
|
||||
return function (this: unknown, ...args: Parameters<F>) {
|
||||
const traceName = mapper?.(...args) ?? name;
|
||||
|
||||
beginTrace(traceName, ...arguments);
|
||||
try {
|
||||
return [f.apply(this, args), finishTrace(traceName)];
|
||||
} catch (e) {
|
||||
finishTrace(traceName);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const traceFunction = !(IS_DEV || IS_REPORTER)
|
||||
? noopTracer
|
||||
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
|
||||
return function (this: any, ...args: Parameters<F>) {
|
||||
return function (this: unknown, ...args: Parameters<F>) {
|
||||
const traceName = mapper?.(...args) ?? name;
|
||||
|
||||
beginTrace(traceName, ...arguments);
|
||||
|
|
|
@ -8,23 +8,27 @@ import { Logger } from "@utils/Logger";
|
|||
import { canonicalizeMatch } from "@utils/patches";
|
||||
import * as Webpack from "@webpack";
|
||||
import { wreq } from "@webpack";
|
||||
|
||||
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
|
||||
import { AnyModuleFactory, ModuleFactory } from "webpack";
|
||||
|
||||
export async function loadLazyChunks() {
|
||||
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
|
||||
|
||||
try {
|
||||
LazyChunkLoaderLogger.log("Loading all chunks...");
|
||||
|
||||
const validChunks = new Set<number>();
|
||||
const invalidChunks = new Set<number>();
|
||||
const deferredRequires = new Set<number>();
|
||||
const validChunks = new Set<PropertyKey>();
|
||||
const invalidChunks = new Set<PropertyKey>();
|
||||
const deferredRequires = new Set<PropertyKey>();
|
||||
|
||||
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
||||
let chunksSearchingResolve: (value: void) => void;
|
||||
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
||||
|
||||
// True if resolved, false otherwise
|
||||
const chunksSearchPromises = [] as Array<() => boolean>;
|
||||
|
||||
/* This regex loads all language packs which makes webpack finds testing extremely slow, so for now, lets use one which doesnt include those
|
||||
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
|
||||
*/
|
||||
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
|
||||
|
||||
let foundCssDebuggingLoad = false;
|
||||
|
@ -34,12 +38,15 @@ export async function loadLazyChunks() {
|
|||
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
|
||||
|
||||
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
||||
const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>();
|
||||
const validChunkGroups = new Set<[chunkIds: PropertyKey[], entryPoint: PropertyKey]>();
|
||||
|
||||
const shouldForceDefer = false;
|
||||
|
||||
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
||||
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
|
||||
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => {
|
||||
const numChunkId = Number(m[1]);
|
||||
return Number.isNaN(numChunkId) ? m[1] : numChunkId;
|
||||
}) : [];
|
||||
|
||||
if (chunkIds.length === 0) {
|
||||
return;
|
||||
|
@ -74,7 +81,8 @@ export async function loadLazyChunks() {
|
|||
}
|
||||
|
||||
if (!invalidChunkGroup) {
|
||||
validChunkGroups.add([chunkIds, Number(entryPoint)]);
|
||||
const numEntryPoint = Number(entryPoint);
|
||||
validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : numEntryPoint]);
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -82,7 +90,7 @@ export async function loadLazyChunks() {
|
|||
await Promise.all(
|
||||
Array.from(validChunkGroups)
|
||||
.map(([chunkIds]) =>
|
||||
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
||||
Promise.all(chunkIds.map(id => wreq.e(id)))
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -94,7 +102,7 @@ export async function loadLazyChunks() {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||
if (wreq.m[entryPoint]) wreq(entryPoint);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
@ -122,41 +130,44 @@ export async function loadLazyChunks() {
|
|||
}, 0);
|
||||
}
|
||||
|
||||
Webpack.factoryListeners.add(factory => {
|
||||
function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
|
||||
let isResolved = false;
|
||||
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
||||
searchAndLoadLazyChunks(String(factory))
|
||||
.then(() => isResolved = true)
|
||||
.catch(() => isResolved = true);
|
||||
|
||||
chunksSearchPromises.push(() => isResolved);
|
||||
});
|
||||
}
|
||||
|
||||
Webpack.factoryListeners.add(factoryListener);
|
||||
for (const factoryId in wreq.m) {
|
||||
let isResolved = false;
|
||||
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
||||
|
||||
chunksSearchPromises.push(() => isResolved);
|
||||
factoryListener(wreq.m[factoryId]);
|
||||
}
|
||||
|
||||
await chunksSearchingDone;
|
||||
Webpack.factoryListeners.delete(factoryListener);
|
||||
|
||||
// Require deferred entry points
|
||||
for (const deferredRequire of deferredRequires) {
|
||||
wreq!(deferredRequire as any);
|
||||
wreq(deferredRequire);
|
||||
}
|
||||
|
||||
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
||||
const allChunks = [] as number[];
|
||||
const allChunks = [] as PropertyKey[];
|
||||
|
||||
// Matches "id" or id:
|
||||
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
|
||||
for (const currentMatch of String(wreq.u).matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
|
||||
const id = currentMatch[1] ?? currentMatch[2];
|
||||
if (id == null) continue;
|
||||
|
||||
allChunks.push(Number(id));
|
||||
const numId = Number(id);
|
||||
allChunks.push(Number.isNaN(numId) ? id : numId);
|
||||
}
|
||||
|
||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||
|
||||
// Chunks that are not loaded (not used) by Discord code anymore
|
||||
// Chunks which our regex could not catch to load
|
||||
// It will always contain WebWorker assets, and also currently contains some language packs which are loaded differently
|
||||
const chunksLeft = allChunks.filter(id => {
|
||||
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||
});
|
||||
|
@ -166,12 +177,9 @@ export async function loadLazyChunks() {
|
|||
.then(r => r.text())
|
||||
.then(t => t.includes("importScripts("));
|
||||
|
||||
// Loads and requires a chunk
|
||||
// Loads the chunk. Currently this only happens with the language packs which are loaded differently
|
||||
if (!isWorkerAsset) {
|
||||
await wreq.e(id as any);
|
||||
// Technically, the id of the chunk does not match the entry point
|
||||
// But, still try it because we have no way to get the actual entry point
|
||||
if (wreq.m[id]) wreq(id as any);
|
||||
await wreq.e(id);
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
|
@ -6,28 +6,57 @@
|
|||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import * as Webpack from "@webpack";
|
||||
import { patches } from "plugins";
|
||||
import { addPatch, patches } from "plugins";
|
||||
import { getBuildNumber } from "webpack/patchWebpack";
|
||||
|
||||
import { loadLazyChunks } from "./loadLazyChunks";
|
||||
|
||||
async function runReporter() {
|
||||
const ReporterLogger = new Logger("Reporter");
|
||||
|
||||
async function runReporter() {
|
||||
try {
|
||||
ReporterLogger.log("Starting test...");
|
||||
|
||||
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
|
||||
let loadLazyChunksResolve: (value: void) => void;
|
||||
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
|
||||
|
||||
Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
|
||||
// The main patch for starting the reporter chunk loading
|
||||
addPatch({
|
||||
find: '"Could not find app-mount"',
|
||||
replacement: {
|
||||
match: /(?<="use strict";)/,
|
||||
replace: "Vencord.Webpack._initReporter();"
|
||||
}
|
||||
}, "Vencord Reporter");
|
||||
|
||||
// @ts-ignore
|
||||
Vencord.Webpack._initReporter = function () {
|
||||
// initReporter is called in the patched entry point of Discord
|
||||
// setImmediate to only start searching for lazy chunks after Discord initialized the app
|
||||
setTimeout(() => loadLazyChunks().then(loadLazyChunksResolve), 0);
|
||||
};
|
||||
|
||||
await loadLazyChunksDone;
|
||||
|
||||
if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {
|
||||
console.log("[REPORTER_META]", {
|
||||
buildNumber: getBuildNumber(),
|
||||
buildHash: window.GLOBAL_ENV.SENTRY_TAGS.buildId
|
||||
});
|
||||
}
|
||||
|
||||
for (const patch of patches) {
|
||||
if (!patch.all) {
|
||||
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [plugin, moduleId, match, totalTime] of Vencord.WebpackPatcher.patchTimings) {
|
||||
if (totalTime > 5) {
|
||||
new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
|
||||
let method = searchType;
|
||||
|
||||
|
@ -88,4 +117,6 @@ async function runReporter() {
|
|||
}
|
||||
}
|
||||
|
||||
runReporter();
|
||||
// Run after the Vencord object has been created.
|
||||
// We need to add extra properties to it, and it is only created after all of Vencord code has ran
|
||||
setTimeout(runReporter, 0);
|
||||
|
|
7
src/globals.d.ts
vendored
7
src/globals.d.ts
vendored
|
@ -64,13 +64,8 @@ declare global {
|
|||
export var Vesktop: any;
|
||||
export var VesktopNative: any;
|
||||
|
||||
interface Window {
|
||||
webpackChunkdiscord_app: {
|
||||
push(chunk: any): any;
|
||||
pop(): any;
|
||||
};
|
||||
interface Window extends Record<PropertyKey, any> {
|
||||
_: LoDashStatic;
|
||||
[k: string]: any;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||
import { WebpackRequire } from "webpack";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
disableAnalytics: {
|
||||
|
@ -81,9 +82,9 @@ export default definePlugin({
|
|||
Object.defineProperty(Function.prototype, "g", {
|
||||
configurable: true,
|
||||
|
||||
set(v: any) {
|
||||
set(this: WebpackRequire, globalObj: WebpackRequire["g"]) {
|
||||
Object.defineProperty(this, "g", {
|
||||
value: v,
|
||||
value: globalObj,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true
|
||||
|
@ -92,11 +93,11 @@ export default definePlugin({
|
|||
// Ensure this is most likely the Sentry WebpackInstance.
|
||||
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it
|
||||
const { stack } = new Error();
|
||||
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) {
|
||||
if (this.c != null || !stack?.includes("http") || !String(this).includes("exports:{}")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0];
|
||||
const assetPath = stack.match(/http.+?(?=:\d+?:\d+?$)/m)?.[0];
|
||||
if (!assetPath) {
|
||||
return;
|
||||
}
|
||||
|
@ -106,7 +107,8 @@ export default definePlugin({
|
|||
srcRequest.send();
|
||||
|
||||
// Final condition to see if this is the Sentry WebpackInstance
|
||||
if (!srcRequest.responseText.includes("window.DiscordSentry=")) {
|
||||
// This is matching window.DiscordSentry=, but without `window` to avoid issues on some proxies
|
||||
if (!srcRequest.responseText.includes(".DiscordSentry=")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Flex } from "@components/Flex";
|
||||
import { Link } from "@components/Link";
|
||||
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
||||
import { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, KNOWN_ISSUES_CHANNEL_ID, REGULAR_ROLE_ID, SUPPORT_CHANNEL_ID, VENBOT_USER_ID, VENCORD_GUILD_ID } from "@utils/constants";
|
||||
import { sendMessage } from "@utils/discord";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
|
@ -40,9 +40,6 @@ import plugins, { PluginMeta } from "~plugins";
|
|||
|
||||
import SettingsPlugin from "./settings";
|
||||
|
||||
const VENCORD_GUILD_ID = "1015060230222131221";
|
||||
const VENBOT_USER_ID = "1017176847865352332";
|
||||
const KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920";
|
||||
const CodeBlockRe = /```js\n(.+?)```/s;
|
||||
|
||||
const AllowedChannelIds = [
|
||||
|
@ -52,9 +49,9 @@ const AllowedChannelIds = [
|
|||
];
|
||||
|
||||
const TrustedRolesIds = [
|
||||
"1026534353167208489", // contributor
|
||||
"1026504932959977532", // regular
|
||||
"1042507929485586532", // donor
|
||||
CONTRIB_ROLE_ID, // contributor
|
||||
REGULAR_ROLE_ID, // regular
|
||||
DONOR_ROLE_ID, // donor
|
||||
];
|
||||
|
||||
const AsyncFunction = async function () { }.constructor;
|
||||
|
|
|
@ -82,6 +82,8 @@ function makeShortcuts() {
|
|||
wp: Webpack,
|
||||
wpc: { getter: () => Webpack.cache },
|
||||
wreq: { getter: () => Webpack.wreq },
|
||||
wpPatcher: { getter: () => Vencord.WebpackPatcher },
|
||||
wpInstances: { getter: () => Vencord.WebpackPatcher.allWebpackInstances },
|
||||
wpsearch: search,
|
||||
wpex: extract,
|
||||
wpexs: (code: string) => extract(findModuleId(code)!),
|
||||
|
|
|
@ -160,7 +160,7 @@ function initWs(isManual = false) {
|
|||
return reply("Expected exactly one 'find' matches, found " + keys.length);
|
||||
|
||||
const mod = candidates[keys[0]];
|
||||
let src = String(mod.original ?? mod).replaceAll("\n", "");
|
||||
let src = String(mod).replaceAll("\n", "");
|
||||
|
||||
if (src.startsWith("function(")) {
|
||||
src = "0," + src;
|
||||
|
|
|
@ -35,8 +35,10 @@ import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/Messag
|
|||
import { Settings, SettingsStore } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { canonicalizeFind } from "@utils/patches";
|
||||
|
||||
import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches";
|
||||
import { OptionType, Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
|
||||
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
import { FluxEvents } from "@webpack/types";
|
||||
|
||||
|
@ -65,7 +67,7 @@ export function isPluginEnabled(p: string) {
|
|||
) ?? false;
|
||||
}
|
||||
|
||||
export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
|
||||
export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string, pluginPath = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`) {
|
||||
const patch = newPatch as Patch;
|
||||
patch.plugin = pluginName;
|
||||
|
||||
|
@ -81,10 +83,12 @@ export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
|
|||
patch.replacement = [patch.replacement];
|
||||
}
|
||||
|
||||
for (const replacement of patch.replacement) {
|
||||
canonicalizeReplacement(replacement, pluginPath);
|
||||
|
||||
if (IS_REPORTER) {
|
||||
patch.replacement.forEach(r => {
|
||||
delete r.predicate;
|
||||
});
|
||||
delete replacement.predicate;
|
||||
}
|
||||
}
|
||||
|
||||
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());
|
||||
|
|
|
@ -124,11 +124,11 @@ export default definePlugin({
|
|||
},
|
||||
// Voice Users
|
||||
{
|
||||
find: "renderPrioritySpeaker(){",
|
||||
find: ".usernameSpeaking]:",
|
||||
replacement: [
|
||||
{
|
||||
match: /renderName\(\){.+?usernameSpeaking\]:.+?(?=children)/,
|
||||
replace: "$&style:$self.getColorStyle(this?.props?.user?.id,this?.props?.guildId),"
|
||||
match: /\.usernameSpeaking\]:.+?,(?=children)(?<=guildId:(\i),.+?user:(\i).+?)/,
|
||||
replace: "$&style:$self.getColorStyle($2.id,$1),"
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.voiceUsers
|
||||
|
|
|
@ -32,7 +32,7 @@ export class Logger {
|
|||
constructor(public name: string, public color: string = "white") { }
|
||||
|
||||
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
|
||||
if (IS_REPORTER && IS_WEB) {
|
||||
if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {
|
||||
console[level]("[Vencord]", this.name + ":", ...args);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -16,9 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const WEBPACK_CHUNK = "webpackChunkdiscord_app";
|
||||
export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
|
||||
export const VENBOT_USER_ID = "1017176847865352332";
|
||||
export const VENCORD_GUILD_ID = "1015060230222131221";
|
||||
export const DONOR_ROLE_ID = "1042507929485586532";
|
||||
export const CONTRIB_ROLE_ID = "1026534353167208489";
|
||||
export const REGULAR_ROLE_ID = "1026504932959977532";
|
||||
export const SUPPORT_CHANNEL_ID = "1026515880080842772";
|
||||
export const KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920";
|
||||
|
||||
export interface Dev {
|
||||
name: string;
|
||||
|
@ -579,6 +584,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "jamesbt365",
|
||||
id: 158567567487795200n,
|
||||
},
|
||||
samsam: {
|
||||
name: "samsam",
|
||||
id: 836452332387565589n,
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
|
@ -100,6 +100,11 @@ export function pluralise(amount: number, singular: string, plural = singular +
|
|||
return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;
|
||||
}
|
||||
|
||||
export function interpolateIfDefined(strings: TemplateStringsArray, ...args: any[]) {
|
||||
if (args.some(arg => arg == null)) return "";
|
||||
return String.raw({ raw: strings }, ...args);
|
||||
}
|
||||
|
||||
export function tryOrElse<T>(func: () => T, fallback: T): T {
|
||||
try {
|
||||
const res = func();
|
||||
|
|
|
@ -41,16 +41,17 @@ export function canonicalizeMatch<T extends RegExp | string>(match: T): T {
|
|||
}
|
||||
|
||||
const canonSource = partialCanon.replaceAll("\\i", String.raw`(?:[A-Za-z_$][\w$]*)`);
|
||||
return new RegExp(canonSource, match.flags) as T;
|
||||
const canonRegex = new RegExp(canonSource, match.flags);
|
||||
canonRegex.toString = match.toString.bind(match);
|
||||
|
||||
return canonRegex as T;
|
||||
}
|
||||
|
||||
export function canonicalizeReplace<T extends string | ReplaceFn>(replace: T, pluginName: string): T {
|
||||
const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`;
|
||||
|
||||
export function canonicalizeReplace<T extends string | ReplaceFn>(replace: T, pluginPath: string): T {
|
||||
if (typeof replace !== "function")
|
||||
return replace.replaceAll("$self", self) as T;
|
||||
return replace.replaceAll("$self", pluginPath) as T;
|
||||
|
||||
return ((...args) => replace(...args).replaceAll("$self", self)) as T;
|
||||
return ((...args) => replace(...args).replaceAll("$self", pluginPath)) as T;
|
||||
}
|
||||
|
||||
export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) {
|
||||
|
@ -65,12 +66,12 @@ export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>
|
|||
return descriptor;
|
||||
}
|
||||
|
||||
export function canonicalizeReplacement(replacement: Pick<PatchReplacement, "match" | "replace">, plugin: string) {
|
||||
export function canonicalizeReplacement(replacement: Pick<PatchReplacement, "match" | "replace">, pluginPath: string) {
|
||||
const descriptors = Object.getOwnPropertyDescriptors(replacement);
|
||||
descriptors.match = canonicalizeDescriptor(descriptors.match, canonicalizeMatch);
|
||||
descriptors.replace = canonicalizeDescriptor(
|
||||
descriptors.replace,
|
||||
replace => canonicalizeReplace(replace, plugin),
|
||||
replace => canonicalizeReplace(replace, pluginPath),
|
||||
);
|
||||
Object.defineProperties(replacement, descriptors);
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ export async function downloadSettingsBackup() {
|
|||
}
|
||||
}
|
||||
|
||||
const toast = (type: number, message: string) =>
|
||||
const toast = (type: string, message: string) =>
|
||||
Toasts.show({
|
||||
type,
|
||||
message,
|
||||
|
|
|
@ -43,6 +43,10 @@ export interface PatchReplacement {
|
|||
replace: string | ReplaceFn;
|
||||
/** A function which returns whether this patch replacement should be applied */
|
||||
predicate?(): boolean;
|
||||
/** The minimum build number for this patch to be applied */
|
||||
fromBuild?: number;
|
||||
/** The maximum build number for this patch to be applied */
|
||||
toBuild?: number;
|
||||
}
|
||||
|
||||
export interface Patch {
|
||||
|
@ -59,6 +63,10 @@ export interface Patch {
|
|||
group?: boolean;
|
||||
/** A function which returns whether this patch should be applied */
|
||||
predicate?(): boolean;
|
||||
/** The minimum build number for this patch to be applied */
|
||||
fromBuild?: number;
|
||||
/** The maximum build number for this patch to be applied */
|
||||
toBuild?: number;
|
||||
}
|
||||
|
||||
export interface PluginAuthor {
|
||||
|
|
|
@ -25,7 +25,7 @@ export const Menu = {} as t.Menu;
|
|||
// Relies on .name properties added by the MenuItemDemanglerAPI
|
||||
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
|
||||
// we have to do this manual require by ID because m is in this case the MenuCheckBoxItem instead of the entire module
|
||||
const module = wreq(id as any);
|
||||
const module = wreq(id);
|
||||
|
||||
for (const e of Object.values(module)) {
|
||||
if (typeof e === "function" && e.name.startsWith("Menu")) {
|
||||
|
|
6
src/webpack/common/types/components.d.ts
vendored
6
src/webpack/common/types/components.d.ts
vendored
|
@ -152,7 +152,7 @@ export type ComboboxPopout = ComponentType<PropsWithChildren<{
|
|||
|
||||
}>>;
|
||||
|
||||
export type Button = ComponentType<PropsWithChildren<Omit<HTMLProps<HTMLButtonElement>, "size"> & {
|
||||
export interface ButtonProps extends PropsWithChildren<Omit<HTMLProps<HTMLButtonElement>, "size">> {
|
||||
/** Button.Looks.FILLED */
|
||||
look?: string;
|
||||
/** Button.Colors.BRAND */
|
||||
|
@ -172,7 +172,9 @@ export type Button = ComponentType<PropsWithChildren<Omit<HTMLProps<HTMLButtonEl
|
|||
|
||||
submittingStartedLabel?: string;
|
||||
submittingFinishedLabel?: string;
|
||||
}>> & {
|
||||
}
|
||||
|
||||
export type Button = ComponentType<ButtonProps> & {
|
||||
BorderColors: Record<"BLACK" | "BRAND" | "BRAND_NEW" | "GREEN" | "LINK" | "PRIMARY" | "RED" | "TRANSPARENT" | "WHITE" | "YELLOW", string>;
|
||||
Colors: Record<"BRAND" | "RED" | "GREEN" | "YELLOW" | "PRIMARY" | "LINK" | "WHITE" | "BLACK" | "TRANSPARENT" | "BRAND_NEW" | "CUSTOM", string>;
|
||||
Hovers: Record<"DEFAULT" | "BRAND" | "RED" | "GREEN" | "YELLOW" | "PRIMARY" | "LINK" | "WHITE" | "BLACK" | "TRANSPARENT", string>;
|
||||
|
|
|
@ -71,10 +71,15 @@ export let Alerts: t.Alerts;
|
|||
waitFor(["show", "close"], m => Alerts = m);
|
||||
|
||||
const ToastType = {
|
||||
MESSAGE: 0,
|
||||
SUCCESS: 1,
|
||||
FAILURE: 2,
|
||||
CUSTOM: 3
|
||||
MESSAGE: "message",
|
||||
SUCCESS: "success",
|
||||
FAILURE: "failure",
|
||||
CUSTOM: "custom",
|
||||
CLIP: "clip",
|
||||
LINK: "link",
|
||||
FORWARD: "forward",
|
||||
BOOKMARK: "bookmark",
|
||||
CLOCK: "clock"
|
||||
};
|
||||
const ToastPosition = {
|
||||
TOP: 0,
|
||||
|
@ -87,7 +92,7 @@ export interface ToastData {
|
|||
/**
|
||||
* Toasts.Type
|
||||
*/
|
||||
type: number,
|
||||
type: string,
|
||||
options?: ToastOptions;
|
||||
}
|
||||
|
||||
|
@ -110,7 +115,7 @@ export const Toasts = {
|
|||
...{} as {
|
||||
show(data: ToastData): void;
|
||||
pop(): void;
|
||||
create(message: string, type: number, options?: ToastOptions): ToastData;
|
||||
create(message: string, type: string, options?: ToastOptions): ToastData;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
|
||||
export * as Common from "./common";
|
||||
export * from "./webpack";
|
||||
export * from "./wreq.d";
|
||||
|
|
|
@ -1,189 +1,366 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated, Nuckyz, and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { WEBPACK_CHUNK } from "@utils/constants";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { makeLazy } from "@utils/lazy";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { interpolateIfDefined } from "@utils/misc";
|
||||
import { canonicalizeReplacement } from "@utils/patches";
|
||||
import { PatchReplacement } from "@utils/types";
|
||||
import { WebpackInstance } from "discord-types/other";
|
||||
|
||||
import { traceFunction } from "../debug/Tracer";
|
||||
import { traceFunctionWithResults } from "../debug/Tracer";
|
||||
import { patches } from "../plugins";
|
||||
import { _initWebpack, _shouldIgnoreModule, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from ".";
|
||||
import { _initWebpack, _shouldIgnoreModule, AnyModuleFactory, AnyWebpackRequire, factoryListeners, findModuleId, MaybeWrappedModuleFactory, ModuleExports, moduleListeners, waitForSubscriptions, WebpackRequire, WrappedModuleFactory, wreq } from ".";
|
||||
|
||||
export const SYM_ORIGINAL_FACTORY = Symbol("WebpackPatcher.originalFactory");
|
||||
export const SYM_PATCHED_SOURCE = Symbol("WebpackPatcher.patchedSource");
|
||||
export const SYM_PATCHED_BY = Symbol("WebpackPatcher.patchedBy");
|
||||
/** A set with all the Webpack instances */
|
||||
export const allWebpackInstances = new Set<AnyWebpackRequire>();
|
||||
export const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: string | RegExp, totalTime: number]>;
|
||||
|
||||
const logger = new Logger("WebpackInterceptor", "#8caaee");
|
||||
/** Whether we tried to fallback to factory WebpackRequire, or disabled patches */
|
||||
let wreqFallbackApplied = false;
|
||||
/** Whether we should be patching factories.
|
||||
*
|
||||
* This should be disabled if we start searching for the module to get the build number, and then resumed once it's done.
|
||||
* */
|
||||
let shouldPatchFactories = true;
|
||||
|
||||
let webpackChunk: any[];
|
||||
export const getBuildNumber = makeLazy(() => {
|
||||
try {
|
||||
shouldPatchFactories = false;
|
||||
|
||||
// Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed
|
||||
// This way we can patch the factory of everything being pushed to the modules array
|
||||
Object.defineProperty(window, WEBPACK_CHUNK, {
|
||||
configurable: true,
|
||||
try {
|
||||
if (wreq.m[128014]?.toString().includes("Trying to open a changelog for an invalid build number")) {
|
||||
const hardcodedGetBuildNumber = wreq(128014).b as () => number;
|
||||
|
||||
get: () => webpackChunk,
|
||||
set: v => {
|
||||
if (v?.push) {
|
||||
if (!v.push.$$vencordOriginal) {
|
||||
logger.info(`Patching ${WEBPACK_CHUNK}.push`);
|
||||
patchPush(v);
|
||||
|
||||
// @ts-ignore
|
||||
delete window[WEBPACK_CHUNK];
|
||||
window[WEBPACK_CHUNK] = v;
|
||||
if (typeof hardcodedGetBuildNumber === "function" && typeof hardcodedGetBuildNumber() === "number") {
|
||||
return hardcodedGetBuildNumber();
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
webpackChunk = v;
|
||||
const moduleId = findModuleId("Trying to open a changelog for an invalid build number");
|
||||
if (moduleId == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const exports = Object.values<ModuleExports>(wreq(moduleId));
|
||||
if (exports.length !== 1 || typeof exports[0] !== "function") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const buildNumber = exports[0]();
|
||||
return typeof buildNumber === "number" ? buildNumber : -1;
|
||||
} catch {
|
||||
return -1;
|
||||
} finally {
|
||||
shouldPatchFactories = true;
|
||||
}
|
||||
});
|
||||
|
||||
// wreq.m is the webpack module factory.
|
||||
// normally, this is populated via webpackGlobal.push, which we patch below.
|
||||
// However, Discord has their .m prepopulated.
|
||||
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
|
||||
Object.defineProperty(Function.prototype, "m", {
|
||||
configurable: true,
|
||||
type Define = typeof Reflect.defineProperty;
|
||||
const define: Define = (target, p, attributes) => {
|
||||
if (Object.hasOwn(attributes, "value")) {
|
||||
attributes.writable = true;
|
||||
}
|
||||
|
||||
set(v: any) {
|
||||
Object.defineProperty(this, "m", {
|
||||
value: v,
|
||||
return Reflect.defineProperty(target, p, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true
|
||||
...attributes
|
||||
});
|
||||
};
|
||||
|
||||
// When using react devtools or other extensions, we may also catch their webpack here.
|
||||
// This ensures we actually got the right one
|
||||
export function getOriginalFactory(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
|
||||
const moduleFactory = webpackRequire.m[id];
|
||||
return (moduleFactory?.[SYM_ORIGINAL_FACTORY] ?? moduleFactory) as AnyModuleFactory | undefined;
|
||||
}
|
||||
|
||||
export function getFactoryPatchedSource(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
|
||||
return webpackRequire.m[id]?.[SYM_PATCHED_SOURCE];
|
||||
}
|
||||
|
||||
export function getFactoryPatchedBy(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
|
||||
return webpackRequire.m[id]?.[SYM_PATCHED_BY];
|
||||
}
|
||||
|
||||
// wreq.m is the Webpack object containing module factories. It is pre-populated with module factories, and is also populated via webpackGlobal.push
|
||||
// We use this setter to intercept when wreq.m is defined and apply the patching in its module factories.
|
||||
// We wrap wreq.m with our proxy, which is responsible for patching the module factories when they are set, or defining getters for the patched versions.
|
||||
|
||||
// If this is the main Webpack, we also set up the internal references to WebpackRequire.
|
||||
define(Function.prototype, "m", {
|
||||
enumerable: false,
|
||||
|
||||
set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire["m"]) {
|
||||
define(this, "m", { value: originalModules });
|
||||
|
||||
// Ensure this is one of Discord main Webpack instances.
|
||||
// We may catch Discord bundled libs, React Devtools or other extensions Webpack instances here.
|
||||
const { stack } = new Error();
|
||||
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(v)) {
|
||||
if (!stack?.includes("http") || stack.match(/at \d+? \(/) || !String(this).includes("exports:{}")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "";
|
||||
logger.info("Found Webpack module factory", fileName);
|
||||
const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1];
|
||||
logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`);
|
||||
|
||||
patchFactories(v);
|
||||
allWebpackInstances.add(this);
|
||||
|
||||
// Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property.
|
||||
// Define a setter for the ensureChunk property of WebpackRequire. Only the main Webpack (which is the only that includes chunk loading) has this property.
|
||||
// So if the setter is called, this means we can initialize the internal references to WebpackRequire.
|
||||
Object.defineProperty(this, "p", {
|
||||
configurable: true,
|
||||
|
||||
set(this: WebpackInstance, bundlePath: string) {
|
||||
Object.defineProperty(this, "p", {
|
||||
value: bundlePath,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true
|
||||
});
|
||||
define(this, "e", {
|
||||
enumerable: false,
|
||||
|
||||
set(this: WebpackRequire, ensureChunk: WebpackRequire["e"]) {
|
||||
define(this, "e", { value: ensureChunk });
|
||||
clearTimeout(setterTimeout);
|
||||
if (bundlePath !== "/assets/") return;
|
||||
|
||||
logger.info(`Main Webpack found in ${fileName}, initializing internal references to WebpackRequire`);
|
||||
logger.info("Main WebpackInstance found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire");
|
||||
_initWebpack(this);
|
||||
|
||||
for (const beforeInitListener of beforeInitListeners) {
|
||||
beforeInitListener(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
// setImmediate to clear this property setter if this is not the main Webpack.
|
||||
// If this is the main Webpack, wreq.p will always be set before the timeout runs.
|
||||
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0);
|
||||
// If this is the main Webpack, wreq.e will always be set before the timeout runs.
|
||||
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "e"), 0);
|
||||
|
||||
// Patch the pre-populated factories
|
||||
for (const id in originalModules) {
|
||||
if (updateExistingFactory(originalModules, id, originalModules[id], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
notifyFactoryListeners(originalModules[id]);
|
||||
defineModulesFactoryGetter(id, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(id, originalModules[id]) : originalModules[id]);
|
||||
}
|
||||
|
||||
define(originalModules, Symbol.toStringTag, {
|
||||
value: "ModuleFactories",
|
||||
enumerable: false
|
||||
});
|
||||
|
||||
// The proxy responsible for patching the module factories when they are set, or defining getters for the patched versions
|
||||
const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler);
|
||||
/*
|
||||
If Webpack ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype
|
||||
Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler));
|
||||
*/
|
||||
|
||||
define(this, "m", { value: proxiedModuleFactories });
|
||||
}
|
||||
});
|
||||
|
||||
function patchPush(webpackGlobal: any) {
|
||||
function handlePush(chunk: any) {
|
||||
const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = {
|
||||
/*
|
||||
If Webpack ever decides to set module factories using the variable of the modules object directly instead of wreq.m, we need to switch the proxy to the prototype
|
||||
and that requires defining additional traps for keeping the object working
|
||||
|
||||
// Proxies on the prototype don't intercept "get" when the property is in the object itself. But in case it isn't we need to return undefined,
|
||||
// to avoid Reflect.get having no effect and causing a stack overflow
|
||||
get(target, p, receiver) {
|
||||
return undefined;
|
||||
},
|
||||
// Same thing as get
|
||||
has(target, p) {
|
||||
return false;
|
||||
},
|
||||
*/
|
||||
|
||||
// The set trap for patching or defining getters for the module factories when new module factories are loaded
|
||||
set(target, p, newValue, receiver) {
|
||||
if (updateExistingFactory(target, p, newValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
notifyFactoryListeners(newValue);
|
||||
defineModulesFactoryGetter(p, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(p, newValue) : newValue);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a factory that exists in any Webpack instance with a new original factory.
|
||||
*
|
||||
* @target The module factories where this new original factory is being set
|
||||
* @param id The id of the module
|
||||
* @param newFactory The new original factory
|
||||
* @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactoriesTarget
|
||||
* @returns Whether the original factory was updated, or false if it doesn't exist in any Webpack instance
|
||||
*/
|
||||
function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id: PropertyKey, newFactory: AnyModuleFactory, ignoreExistingInTarget: boolean = false) {
|
||||
let existingFactory: TypedPropertyDescriptor<AnyModuleFactory> | undefined;
|
||||
let moduleFactoriesWithFactory: AnyWebpackRequire["m"] | undefined;
|
||||
for (const wreq of allWebpackInstances) {
|
||||
if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) continue;
|
||||
|
||||
if (Object.hasOwn(wreq.m, id)) {
|
||||
existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, id);
|
||||
moduleFactoriesWithFactory = wreq.m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingFactory != null) {
|
||||
// If existingFactory exists in any Webpack instance, it's either wrapped in defineModuleFactoryGetter, or it has already been required.
|
||||
// So define the descriptor of it on this current Webpack instance (if it doesn't exist already), call Reflect.set with the new original,
|
||||
// and let the correct logic apply (normal set, or defineModuleFactoryGetter setter)
|
||||
|
||||
if (moduleFactoriesWithFactory !== moduleFactoriesTarget) {
|
||||
Reflect.defineProperty(moduleFactoriesTarget, id, existingFactory);
|
||||
}
|
||||
|
||||
// Persist patched source and patched by in the new original factory, if the patched one has already been required
|
||||
if (IS_DEV && existingFactory.value != null) {
|
||||
newFactory[SYM_PATCHED_SOURCE] = existingFactory.value[SYM_PATCHED_SOURCE];
|
||||
newFactory[SYM_PATCHED_BY] = existingFactory.value[SYM_PATCHED_BY];
|
||||
}
|
||||
|
||||
return Reflect.set(moduleFactoriesTarget, id, newFactory, moduleFactoriesTarget);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all factory listeners.
|
||||
*
|
||||
* @param factory The original factory to notify for
|
||||
*/
|
||||
function notifyFactoryListeners(factory: AnyModuleFactory) {
|
||||
for (const factoryListener of factoryListeners) {
|
||||
try {
|
||||
patchFactories(chunk[1]);
|
||||
factoryListener(factory);
|
||||
} catch (err) {
|
||||
logger.error("Error in handlePush", err);
|
||||
logger.error("Error in Webpack factory listener:\n", err, factoryListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handlePush.$$vencordOriginal.call(webpackGlobal, chunk);
|
||||
/**
|
||||
* Define the getter for returning the patched version of the module factory.
|
||||
*
|
||||
* If eagerPatches is enabled, the factory argument should already be the patched version, else it will be the original
|
||||
* and only be patched when accessed for the first time.
|
||||
*
|
||||
* @param id The id of the module
|
||||
* @param factory The original or patched module factory
|
||||
*/
|
||||
function defineModulesFactoryGetter(id: PropertyKey, factory: MaybeWrappedModuleFactory) {
|
||||
const descriptor: PropertyDescriptor = {
|
||||
get() {
|
||||
// SYM_ORIGINAL_FACTORY means the factory is already patched
|
||||
if (!shouldPatchFactories || factory[SYM_ORIGINAL_FACTORY] != null) {
|
||||
return factory;
|
||||
}
|
||||
|
||||
handlePush.$$vencordOriginal = webpackGlobal.push;
|
||||
handlePush.toString = handlePush.$$vencordOriginal.toString.bind(handlePush.$$vencordOriginal);
|
||||
// Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));`
|
||||
// it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush.
|
||||
// If we then repatched the new push, we would end up with recursive patching, which leads to our patches
|
||||
// being applied multiple times.
|
||||
// Thus, override bind to use the original push
|
||||
handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args);
|
||||
|
||||
Object.defineProperty(webpackGlobal, "push", {
|
||||
configurable: true,
|
||||
|
||||
get: () => handlePush,
|
||||
set(v) {
|
||||
handlePush.$$vencordOriginal = v;
|
||||
}
|
||||
});
|
||||
return (factory = wrapAndPatchFactory(id, factory));
|
||||
},
|
||||
set(newFactory: MaybeWrappedModuleFactory) {
|
||||
if (IS_DEV) {
|
||||
newFactory[SYM_PATCHED_SOURCE] = factory[SYM_PATCHED_SOURCE];
|
||||
newFactory[SYM_PATCHED_BY] = factory[SYM_PATCHED_BY];
|
||||
}
|
||||
|
||||
let webpackNotInitializedLogged = false;
|
||||
if (factory[SYM_ORIGINAL_FACTORY] != null) {
|
||||
factory.toString = newFactory.toString.bind(newFactory);
|
||||
factory[SYM_ORIGINAL_FACTORY] = newFactory;
|
||||
} else {
|
||||
factory = newFactory;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function patchFactories(factories: Record<string, (module: any, exports: any, require: WebpackInstance) => void>) {
|
||||
for (const id in factories) {
|
||||
let mod = factories[id];
|
||||
// Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object
|
||||
// have the patched version
|
||||
for (const wreq of allWebpackInstances) {
|
||||
define(wreq.m, id, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
const originalMod = mod;
|
||||
const patchedBy = new Set();
|
||||
/**
|
||||
* Wraps and patches a module factory.
|
||||
*
|
||||
* @param id The id of the module
|
||||
* @param factory The original or patched module factory
|
||||
* @returns The wrapper for the patched module factory
|
||||
*/
|
||||
function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) {
|
||||
const [patchedFactory, patchedSource, patchedBy] = patchFactory(id, originalFactory);
|
||||
|
||||
const factory = factories[id] = function (module: any, exports: any, require: WebpackInstance) {
|
||||
if (wreq == null && IS_DEV) {
|
||||
if (!webpackNotInitializedLogged) {
|
||||
webpackNotInitializedLogged = true;
|
||||
const wrappedFactory: WrappedModuleFactory = function (...args) {
|
||||
// Restore the original factory in all the module factories objects. We want to make sure the original factory is restored properly, no matter what is the Webpack instance
|
||||
for (const wreq of allWebpackInstances) {
|
||||
define(wreq.m, id, { value: wrappedFactory[SYM_ORIGINAL_FACTORY] });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [module, exports, require] = args;
|
||||
|
||||
if (wreq == null) {
|
||||
if (!wreqFallbackApplied) {
|
||||
wreqFallbackApplied = true;
|
||||
|
||||
// Make sure the require argument is actually the WebpackRequire function
|
||||
if (typeof require === "function" && require.m != null) {
|
||||
const { stack } = new Error();
|
||||
const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1];
|
||||
|
||||
logger.warn(
|
||||
"WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" +
|
||||
`id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +
|
||||
")"
|
||||
);
|
||||
|
||||
_initWebpack(require as WebpackRequire);
|
||||
} else if (IS_DEV) {
|
||||
logger.error("WebpackRequire was not initialized, running modules without patches instead.");
|
||||
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args);
|
||||
}
|
||||
} else if (IS_DEV) {
|
||||
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
return void originalMod(module, exports, require);
|
||||
}
|
||||
|
||||
let factoryReturn: unknown;
|
||||
try {
|
||||
mod(module, exports, require);
|
||||
// Call the patched factory
|
||||
factoryReturn = patchedFactory.apply(this, args);
|
||||
} catch (err) {
|
||||
// Just rethrow discord errors
|
||||
if (mod === originalMod) throw err;
|
||||
// Just re-throw Discord errors
|
||||
if (patchedFactory === wrappedFactory[SYM_ORIGINAL_FACTORY]) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
logger.error("Error in patched module", err);
|
||||
return void originalMod(module, exports, require);
|
||||
logger.error("Error in patched module factory:\n", err);
|
||||
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args);
|
||||
}
|
||||
|
||||
exports = module.exports;
|
||||
if (exports == null) {
|
||||
return factoryReturn;
|
||||
}
|
||||
|
||||
if (!exports) return;
|
||||
|
||||
if (require.c) {
|
||||
if (typeof require === "function") {
|
||||
const shouldIgnoreModule = _shouldIgnoreModule(exports);
|
||||
|
||||
if (shouldIgnoreModule) {
|
||||
if (require.c != null) {
|
||||
Object.defineProperty(require.c, id, {
|
||||
value: require.c[id],
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
return factoryReturn;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,11 +372,12 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
|
|||
}
|
||||
}
|
||||
|
||||
for (const [filter, callback] of subscriptions) {
|
||||
for (const [filter, callback] of waitForSubscriptions) {
|
||||
try {
|
||||
if (exports && filter(exports)) {
|
||||
subscriptions.delete(filter);
|
||||
if (filter(exports)) {
|
||||
waitForSubscriptions.delete(filter);
|
||||
callback(exports, id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof exports !== "object") {
|
||||
|
@ -207,66 +385,115 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
|
|||
}
|
||||
|
||||
for (const exportKey in exports) {
|
||||
if (exports[exportKey] && filter(exports[exportKey])) {
|
||||
subscriptions.delete(filter);
|
||||
callback(exports[exportKey], id);
|
||||
const exportValue = exports[exportKey];
|
||||
|
||||
if (exportValue != null && filter(exportValue)) {
|
||||
waitForSubscriptions.delete(filter);
|
||||
callback(exportValue, id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback);
|
||||
}
|
||||
}
|
||||
} as any as { toString: () => string, original: any, (...args: any[]): void; $$vencordPatchedSource?: string; };
|
||||
|
||||
factory.toString = originalMod.toString.bind(originalMod);
|
||||
factory.original = originalMod;
|
||||
|
||||
for (const factoryListener of factoryListeners) {
|
||||
try {
|
||||
factoryListener(originalMod);
|
||||
} catch (err) {
|
||||
logger.error("Error in Webpack factory listener:\n", err, factoryListener);
|
||||
logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Discords Webpack chunks for some ungodly reason contain random
|
||||
// newlines. Cyn recommended this workaround and it seems to work fine,
|
||||
// however this could potentially break code, so if anything goes weird,
|
||||
// this is probably why.
|
||||
// Additionally, `[actual newline]` is one less char than "\n", so if Discord
|
||||
// ever targets newer browsers, the minifier could potentially use this trick and
|
||||
// cause issues.
|
||||
//
|
||||
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
|
||||
let code: string = "0," + mod.toString().replaceAll("\n", "");
|
||||
return factoryReturn;
|
||||
};
|
||||
|
||||
wrappedFactory.toString = originalFactory.toString.bind(originalFactory);
|
||||
wrappedFactory[SYM_ORIGINAL_FACTORY] = originalFactory;
|
||||
|
||||
if (IS_DEV && patchedFactory !== originalFactory) {
|
||||
wrappedFactory[SYM_PATCHED_SOURCE] = patchedSource;
|
||||
wrappedFactory[SYM_PATCHED_BY] = patchedBy;
|
||||
originalFactory[SYM_PATCHED_SOURCE] = patchedSource;
|
||||
originalFactory[SYM_PATCHED_BY] = patchedBy;
|
||||
}
|
||||
|
||||
// @ts-expect-error Allow GC to get into action, if possible
|
||||
originalFactory = undefined;
|
||||
return wrappedFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches a module factory.
|
||||
*
|
||||
* @param id The id of the module
|
||||
* @param factory The original module factory
|
||||
* @returns The patched module factory, the patched source of it, and the plugins that patched it
|
||||
*/
|
||||
function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFactory: AnyModuleFactory, patchedSource: string, patchedBy: Set<string>] {
|
||||
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
|
||||
let code: string = "0," + String(factory);
|
||||
let patchedSource = code;
|
||||
let patchedFactory = factory;
|
||||
|
||||
const patchedBy = new Set<string>();
|
||||
|
||||
for (let i = 0; i < patches.length; i++) {
|
||||
const patch = patches[i];
|
||||
|
||||
const moduleMatches = typeof patch.find === "string"
|
||||
? code.includes(patch.find)
|
||||
: patch.find.test(code);
|
||||
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
|
||||
|
||||
if (!moduleMatches) continue;
|
||||
if (!moduleMatches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
patchedBy.add(patch.plugin);
|
||||
// Reporter eagerly patches and cannot retrieve the build number because this code runs before the module for it is loaded
|
||||
const buildNumber = IS_REPORTER ? -1 : getBuildNumber();
|
||||
const shouldCheckBuildNumber = !Settings.eagerPatches && buildNumber !== -1;
|
||||
|
||||
if (
|
||||
shouldCheckBuildNumber &&
|
||||
(patch.fromBuild != null && buildNumber < patch.fromBuild) ||
|
||||
(patch.toBuild != null && buildNumber > patch.toBuild)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const executePatch = traceFunctionWithResults(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => {
|
||||
if (typeof match !== "string" && match.global) {
|
||||
match.lastIndex = 0;
|
||||
}
|
||||
|
||||
return code.replace(match, replace);
|
||||
});
|
||||
|
||||
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
|
||||
const previousMod = mod;
|
||||
const previousCode = code;
|
||||
const previousFactory = factory;
|
||||
let markedAsPatched = false;
|
||||
|
||||
// We change all patch.replacement to array in plugins/index
|
||||
for (const replacement of patch.replacement as PatchReplacement[]) {
|
||||
const lastMod = mod;
|
||||
const lastCode = code;
|
||||
if (
|
||||
shouldCheckBuildNumber &&
|
||||
(replacement.fromBuild != null && buildNumber < replacement.fromBuild) ||
|
||||
(replacement.toBuild != null && buildNumber > replacement.toBuild)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
canonicalizeReplacement(replacement, patch.plugin);
|
||||
// TODO: remove once Vesktop has been updated to use addPatch
|
||||
if (patch.plugin === "Vesktop") {
|
||||
canonicalizeReplacement(replacement, "VCDP");
|
||||
}
|
||||
|
||||
const lastCode = code;
|
||||
const lastFactory = factory;
|
||||
|
||||
try {
|
||||
const newCode = executePatch(replacement.match, replacement.replace as string);
|
||||
const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string);
|
||||
|
||||
if (IS_REPORTER) {
|
||||
patchTimings.push([patch.plugin, id, replacement.match, totalTime]);
|
||||
}
|
||||
|
||||
if (newCode === code) {
|
||||
if (!patch.noWarn) {
|
||||
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
|
||||
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(id)}): ${replacement.match}`);
|
||||
if (IS_DEV) {
|
||||
logger.debug("Function Source:\n", code);
|
||||
}
|
||||
|
@ -274,9 +501,13 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
|
|||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
|
||||
mod = previousMod;
|
||||
code = previousCode;
|
||||
patchedFactory = previousFactory;
|
||||
|
||||
if (markedAsPatched) {
|
||||
patchedBy.delete(patch.plugin);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -284,13 +515,46 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
|
|||
}
|
||||
|
||||
code = newCode;
|
||||
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
|
||||
patchedSource = `// Webpack Module ${String(id)} - Patched by ${[...patchedBy, patch.plugin].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`;
|
||||
patchedFactory = (0, eval)(patchedSource);
|
||||
|
||||
if (!patchedBy.has(patch.plugin)) {
|
||||
patchedBy.add(patch.plugin);
|
||||
markedAsPatched = true;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err);
|
||||
logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err);
|
||||
|
||||
if (IS_DEV) {
|
||||
diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!);
|
||||
}
|
||||
|
||||
if (markedAsPatched) {
|
||||
patchedBy.delete(patch.plugin);
|
||||
}
|
||||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
||||
code = previousCode;
|
||||
patchedFactory = previousFactory;
|
||||
break;
|
||||
}
|
||||
|
||||
code = lastCode;
|
||||
patchedFactory = lastFactory;
|
||||
}
|
||||
}
|
||||
|
||||
if (!patch.all) {
|
||||
patches.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return [patchedFactory, patchedSource, patchedBy];
|
||||
}
|
||||
|
||||
function diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) {
|
||||
const changeSize = code.length - lastCode.length;
|
||||
const match = lastCode.match(replacement.match)!;
|
||||
|
||||
// Use 200 surrounding characters of context
|
||||
const start = Math.max(0, match.index! - 200);
|
||||
|
@ -301,10 +565,10 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
|
|||
const context = lastCode.slice(start, end);
|
||||
const patchedContext = code.slice(start, endPatched);
|
||||
|
||||
// inline require to avoid including it in !IS_DEV builds
|
||||
// Inline require to avoid including it in !IS_DEV builds
|
||||
const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
|
||||
let fmt = "%c %s ";
|
||||
const elements = [] as string[];
|
||||
const elements: string[] = [];
|
||||
for (const d of diff) {
|
||||
const color = d.removed
|
||||
? "red"
|
||||
|
@ -320,34 +584,3 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
|
|||
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
|
||||
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
|
||||
}
|
||||
|
||||
patchedBy.delete(patch.plugin);
|
||||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
||||
mod = previousMod;
|
||||
code = previousCode;
|
||||
break;
|
||||
}
|
||||
|
||||
mod = lastMod;
|
||||
code = lastCode;
|
||||
}
|
||||
}
|
||||
|
||||
if (!patch.all) patches.splice(i--, 1);
|
||||
}
|
||||
|
||||
if (IS_DEV) {
|
||||
if (mod !== originalMod) {
|
||||
factory.$$vencordPatchedSource = String(mod);
|
||||
} else if (wreq != null) {
|
||||
const existingFactory = wreq.m[id];
|
||||
|
||||
if (existingFactory != null) {
|
||||
factory.$$vencordPatchedSource = existingFactory.$$vencordPatchedSource;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ import { makeLazy, proxyLazy } from "@utils/lazy";
|
|||
import { LazyComponent } from "@utils/lazyReact";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { canonicalizeMatch } from "@utils/patches";
|
||||
import type { WebpackInstance } from "discord-types/other";
|
||||
|
||||
import { traceFunction } from "../debug/Tracer";
|
||||
import { AnyModuleFactory, ModuleExports, WebpackRequire } from "./wreq";
|
||||
|
||||
const logger = new Logger("Webpack");
|
||||
|
||||
|
@ -33,8 +33,8 @@ export let _resolveReady: () => void;
|
|||
*/
|
||||
export const onceReady = new Promise<void>(r => _resolveReady = r);
|
||||
|
||||
export let wreq: WebpackInstance;
|
||||
export let cache: WebpackInstance["c"];
|
||||
export let wreq: WebpackRequire;
|
||||
export let cache: WebpackRequire["c"];
|
||||
|
||||
export type FilterFn = (mod: any) => boolean;
|
||||
|
||||
|
@ -89,16 +89,27 @@ export const filters = {
|
|||
}
|
||||
};
|
||||
|
||||
export type CallbackFn = (mod: any, id: string) => void;
|
||||
export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void;
|
||||
export type FactoryListernFn = (factory: AnyModuleFactory) => void;
|
||||
|
||||
export const subscriptions = new Map<FilterFn, CallbackFn>();
|
||||
export const waitForSubscriptions = new Map<FilterFn, CallbackFn>();
|
||||
export const moduleListeners = new Set<CallbackFn>();
|
||||
export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>();
|
||||
export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>();
|
||||
export const factoryListeners = new Set<FactoryListernFn>();
|
||||
|
||||
export function _initWebpack(webpackRequire: WebpackRequire) {
|
||||
if (webpackRequire.c == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
export function _initWebpack(webpackRequire: WebpackInstance) {
|
||||
wreq = webpackRequire;
|
||||
cache = webpackRequire.c;
|
||||
|
||||
Reflect.defineProperty(webpackRequire.c, Symbol.toStringTag, {
|
||||
value: "ModuleCache",
|
||||
configurable: true,
|
||||
writable: true,
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
// Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too
|
||||
|
@ -531,7 +542,7 @@ export const ChunkIdsRegex = /\("([^"]+?)"\)/g;
|
|||
* @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory
|
||||
* @returns A promise that resolves with a boolean whether the chunks were loaded
|
||||
*/
|
||||
export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = DefaultExtractAndLoadChunksRegex) {
|
||||
export async function extractAndLoadChunks(code: CodeFilter, matcher = DefaultExtractAndLoadChunksRegex) {
|
||||
const module = findModuleFactory(...code);
|
||||
if (!module) {
|
||||
const err = new Error("extractAndLoadChunks: Couldn't find module factory");
|
||||
|
@ -544,7 +555,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
|
|||
return false;
|
||||
}
|
||||
|
||||
const match = module.toString().match(canonicalizeMatch(matcher));
|
||||
const match = String(module).match(canonicalizeMatch(matcher));
|
||||
if (!match) {
|
||||
const err = new Error("extractAndLoadChunks: Couldn't find chunk loading in module factory code");
|
||||
logger.warn(err, "Code:", code, "Matcher:", matcher);
|
||||
|
@ -557,8 +568,9 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
|
|||
}
|
||||
|
||||
const [, rawChunkIds, entryPointId] = match;
|
||||
if (Number.isNaN(Number(entryPointId))) {
|
||||
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
|
||||
|
||||
if (entryPointId == null) {
|
||||
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array or the entry point id");
|
||||
logger.warn(err, "Code:", code, "Matcher:", matcher);
|
||||
|
||||
// Strict behaviour in DevBuilds to fail early and make sure the issue is found
|
||||
|
@ -568,12 +580,19 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
|
|||
return false;
|
||||
}
|
||||
|
||||
const numEntryPoint = Number(entryPointId);
|
||||
const entryPoint = Number.isNaN(numEntryPoint) ? entryPointId : numEntryPoint;
|
||||
|
||||
if (rawChunkIds) {
|
||||
const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => Number(m[1]));
|
||||
const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map(m => {
|
||||
const numChunkId = Number(m[1]);
|
||||
return Number.isNaN(numChunkId) ? m[1] : numChunkId;
|
||||
});
|
||||
|
||||
await Promise.all(chunkIds.map(id => wreq.e(id)));
|
||||
}
|
||||
|
||||
if (wreq.m[entryPointId] == null) {
|
||||
if (wreq.m[entryPoint] == null) {
|
||||
const err = new Error("extractAndLoadChunks: Entry point is not loaded in the module factories, perhaps one of the chunks failed to load");
|
||||
logger.warn(err, "Code:", code, "Matcher:", matcher);
|
||||
|
||||
|
@ -584,7 +603,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
|
|||
return false;
|
||||
}
|
||||
|
||||
wreq(Number(entryPointId));
|
||||
wreq(entryPoint);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -621,7 +640,7 @@ export function waitFor(filter: string | PropsFilter | FilterFn, callback: Callb
|
|||
if (existing) return void callback(existing, id);
|
||||
}
|
||||
|
||||
subscriptions.set(filter, callback);
|
||||
waitForSubscriptions.set(filter, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -637,7 +656,7 @@ export function search(...code: CodeFilter) {
|
|||
const factories = wreq.m;
|
||||
|
||||
for (const id in factories) {
|
||||
const factory = factories[id].original ?? factories[id];
|
||||
const factory = factories[id];
|
||||
|
||||
if (stringMatches(factory.toString(), code))
|
||||
results[id] = factory;
|
||||
|
|
211
src/webpack/wreq.d.ts
vendored
Normal file
211
src/webpack/wreq.d.ts
vendored
Normal file
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated, Nuckyz and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { SYM_ORIGINAL_FACTORY, SYM_PATCHED_BY, SYM_PATCHED_SOURCE } from "./patchWebpack";
|
||||
|
||||
export type ModuleExports = any;
|
||||
|
||||
export type Module = {
|
||||
id: PropertyKey;
|
||||
loaded: boolean;
|
||||
exports: ModuleExports;
|
||||
};
|
||||
|
||||
/** exports can be anything, however initially it is always an empty object */
|
||||
export type ModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: WebpackRequire) => void;
|
||||
|
||||
export type WebpackQueues = unique symbol | "__webpack_queues__";
|
||||
export type WebpackExports = unique symbol | "__webpack_exports__";
|
||||
export type WebpackError = unique symbol | "__webpack_error__";
|
||||
|
||||
export type AsyncModulePromise = Promise<ModuleExports> & {
|
||||
[WebpackQueues]: (fnQueue: ((queue: any[]) => any)) => any;
|
||||
[WebpackExports]: ModuleExports;
|
||||
[WebpackError]?: any;
|
||||
};
|
||||
|
||||
export type AsyncModuleBody = (
|
||||
handleAsyncDependencies: (deps: AsyncModulePromise[]) =>
|
||||
Promise<() => ModuleExports[]> | (() => ModuleExports[]),
|
||||
asyncResult: (error?: any) => void
|
||||
) => Promise<void>;
|
||||
|
||||
export type ChunkHandlers = {
|
||||
/**
|
||||
* Ensures the js file for this chunk is loaded, or starts to load if it's not.
|
||||
* @param chunkId The chunk id
|
||||
* @param promises The promises array to add the loading promise to
|
||||
*/
|
||||
j: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void,
|
||||
/**
|
||||
* Ensures the css file for this chunk is loaded, or starts to load if it's not.
|
||||
* @param chunkId The chunk id
|
||||
* @param promises The promises array to add the loading promise to. This array will likely contain the promise of the js file too
|
||||
*/
|
||||
css: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void,
|
||||
};
|
||||
|
||||
export type ScriptLoadDone = (event: Event) => void;
|
||||
|
||||
// export type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & {
|
||||
// /** Check if a chunk has been loaded */
|
||||
// j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean;
|
||||
// };
|
||||
|
||||
export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & {
|
||||
/** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */
|
||||
m: Record<PropertyKey, ModuleFactory>;
|
||||
/** The module cache, where all modules which have been WebpackRequire'd are stored */
|
||||
c: Record<PropertyKey, Module>;
|
||||
// /**
|
||||
// * Export star. Sets properties of "fromObject" to "toObject" as getters that return the value from "fromObject", like this:
|
||||
// * @example
|
||||
// * const fromObject = { a: 1 };
|
||||
// * Object.keys(fromObject).forEach(key => {
|
||||
// * if (key !== "default" && !Object.hasOwn(toObject, key)) {
|
||||
// * Object.defineProperty(toObject, key, {
|
||||
// * get: () => fromObject[key],
|
||||
// * enumerable: true
|
||||
// * });
|
||||
// * }
|
||||
// * });
|
||||
// * @returns fromObject
|
||||
// */
|
||||
// es: (this: WebpackRequire, fromObject: AnyRecord, toObject: AnyRecord) => AnyRecord;
|
||||
/**
|
||||
* Creates an async module. A module that which has top level await, or requires an export from an async module.
|
||||
*
|
||||
* The body function must be an async function. "module.exports" will become an {@link AsyncModulePromise}.
|
||||
*
|
||||
* The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example on how to handle async dependencies:
|
||||
* @example
|
||||
* const factory = (module, exports, wreq) => {
|
||||
* wreq.a(module, async (handleAsyncDependencies, asyncResult) => {
|
||||
* try {
|
||||
* const asyncRequireA = wreq(...);
|
||||
*
|
||||
* const asyncDependencies = handleAsyncDependencies([asyncRequire]);
|
||||
* const [requireAResult] = asyncDependencies.then != null ? (await asyncDependencies)() : asyncDependencies;
|
||||
*
|
||||
* // Use the required module
|
||||
* console.log(requireAResult);
|
||||
*
|
||||
* // Mark this async module as resolved
|
||||
* asyncResult();
|
||||
* } catch(error) {
|
||||
* // Mark this async module as rejected with an error
|
||||
* asyncResult(error);
|
||||
* }
|
||||
* }, false); // false because our module does not have an await after dealing with the async requires
|
||||
* }
|
||||
*/
|
||||
a: (this: WebpackRequire, module: Module, body: AsyncModuleBody, hasAwaitAfterDependencies?: boolean) => void;
|
||||
/** getDefaultExport function for compatibility with non-harmony modules */
|
||||
n: (this: WebpackRequire, exports: any) => () => ModuleExports;
|
||||
/**
|
||||
* Create a fake namespace object, useful for faking an __esModule with a default export.
|
||||
*
|
||||
* mode & 1: Value is a module id, require it
|
||||
*
|
||||
* mode & 2: Merge all properties of value into the namespace
|
||||
*
|
||||
* mode & 4: Return value when already namespace object
|
||||
*
|
||||
* mode & 16: Return value when it's Promise-like
|
||||
*
|
||||
* mode & (8|1): Behave like require
|
||||
*/
|
||||
t: (this: WebpackRequire, value: any, mode: number) => any;
|
||||
/**
|
||||
* Define getter functions for harmony exports. For every prop in "definiton" (the module exports), set a getter in "exports" for the getter function in the "definition", like this:
|
||||
* @example
|
||||
* const exports = {};
|
||||
* const definition = { exportName: () => someExportedValue };
|
||||
* for (const key in definition) {
|
||||
* if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key)) {
|
||||
* Object.defineProperty(exports, key, {
|
||||
* get: definition[key],
|
||||
* enumerable: true
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* // exports is now { exportName: someExportedValue } (but each value is actually a getter)
|
||||
*/
|
||||
d: (this: WebpackRequire, exports: AnyRecord, definiton: AnyRecord) => void;
|
||||
/** The chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */
|
||||
f: ChunkHandlers;
|
||||
/**
|
||||
* The ensure chunk function, it ensures a chunk is loaded, or loads if needed.
|
||||
* Internally it uses the handlers in {@link WebpackRequire.f} to load/ensure the chunk is loaded.
|
||||
*/
|
||||
e: (this: WebpackRequire, chunkId: PropertyKey) => Promise<void[]>;
|
||||
/** Get the filename for the css part of a chunk */
|
||||
k: (this: WebpackRequire, chunkId: PropertyKey) => string;
|
||||
/** Get the filename for the js part of a chunk */
|
||||
u: (this: WebpackRequire, chunkId: PropertyKey) => string;
|
||||
/** The global object, will likely always be the window */
|
||||
g: typeof globalThis;
|
||||
/** Harmony module decorator. Decorates a module as an ES Module, and prevents Node.js "module.exports" from being set */
|
||||
hmd: (this: WebpackRequire, module: Module) => any;
|
||||
/** Shorthand for Object.prototype.hasOwnProperty */
|
||||
o: typeof Object.prototype.hasOwnProperty;
|
||||
/**
|
||||
* Function to load a script tag. "done" is called when the loading has finished or a timeout has occurred.
|
||||
* "done" will be attached to existing scripts loading if src === url or data-webpack === `${uniqueName}:${key}`,
|
||||
* so it will be called when that existing script finishes loading.
|
||||
*/
|
||||
l: (this: WebpackRequire, url: string, done: ScriptLoadDone, key?: string | number, chunkId?: PropertyKey) => void;
|
||||
/** Defines __esModule on the exports, marking ES Modules compatibility as true */
|
||||
r: (this: WebpackRequire, exports: ModuleExports) => void;
|
||||
/** Node.js module decorator. Decorates a module as a Node.js module */
|
||||
nmd: (this: WebpackRequire, module: Module) => any;
|
||||
// /**
|
||||
// * Register deferred code which will be executed when the passed chunks are loaded.
|
||||
// *
|
||||
// * If chunkIds is defined, it defers the execution of the callback and returns undefined.
|
||||
// *
|
||||
// * If chunkIds is undefined, and no deferred code exists or can be executed, it returns the value of the result argument.
|
||||
// *
|
||||
// * If chunkIds is undefined, and some deferred code can already be executed, it returns the result of the callback function of the last deferred code.
|
||||
// *
|
||||
// * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed.
|
||||
// */
|
||||
// O: OnChunksLoaded;
|
||||
/**
|
||||
* Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports".
|
||||
* @returns The exports argument, but now assigned with the exports of the wasm instance
|
||||
*/
|
||||
v: (this: WebpackRequire, exports: ModuleExports, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise<any>;
|
||||
/** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */
|
||||
p: string;
|
||||
/** The runtime id of the current runtime */
|
||||
j: string;
|
||||
/** Document baseURI or WebWorker location.href */
|
||||
b: string;
|
||||
};
|
||||
|
||||
// Utility section for Vencord
|
||||
|
||||
export type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Partial<Omit<WebpackRequire, "m">> & {
|
||||
/** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */
|
||||
m: Record<PropertyKey, AnyModuleFactory>;
|
||||
};
|
||||
|
||||
/** exports can be anything, however initially it is always an empty object */
|
||||
export type AnyModuleFactory = ((this: ModuleExports, module: Module, exports: ModuleExports, require: AnyWebpackRequire) => void) & {
|
||||
[SYM_PATCHED_SOURCE]?: string;
|
||||
[SYM_PATCHED_BY]?: Set<string>;
|
||||
};
|
||||
|
||||
export type WrappedModuleFactory = AnyModuleFactory & {
|
||||
[SYM_ORIGINAL_FACTORY]: AnyModuleFactory;
|
||||
[SYM_PATCHED_SOURCE]?: string;
|
||||
[SYM_PATCHED_BY]?: Set<string>;
|
||||
};
|
||||
|
||||
export type MaybeWrappedModuleFactory = AnyModuleFactory | WrappedModuleFactory;
|
||||
|
||||
export type WrappedModuleFactories = Record<PropertyKey, WrappedModuleFactory>;
|
Loading…
Add table
Reference in a new issue