Merge branch 'main' into main

This commit is contained in:
Nick 2024-06-19 10:05:53 -04:00 committed by GitHub
commit 8f6384a751
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
213 changed files with 5231 additions and 1643 deletions

View file

@ -1,7 +1,7 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"ignorePatterns": ["dist", "browser"],
"ignorePatterns": ["dist", "browser", "packages/vencord-types"],
"plugins": [
"@typescript-eslint",
"simple-header",

View file

@ -12,7 +12,8 @@ body:
DO NOT USE THIS FORM, unless
- you are a vencord contributor
- you were given explicit permission to use this form by a moderator in our support server
- you are filing a security related report
DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
- type: textarea
id: content

View file

@ -32,7 +32,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Build web
run: pnpm buildWeb --standalone
run: pnpm buildWebStandalone
- name: Build
run: pnpm build --standalone

View file

@ -32,7 +32,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Build web
run: pnpm buildWeb --standalone
run: pnpm buildWebStandalone
- name: Publish extension
run: |

View file

@ -37,8 +37,8 @@ jobs:
with:
chrome-version: stable
- name: Build web
run: pnpm buildWeb --standalone --dev
- name: Build Vencord Reporter Version
run: pnpm buildReporter
- name: Create Report
timeout-minutes: 10

1
.npmrc
View file

@ -1 +1,2 @@
strict-peer-dependencies=false
package-manager-strict=false

View file

@ -14,6 +14,8 @@
"typescript.preferences.quoteStyle": "double",
"javascript.preferences.quoteStyle": "double",
"eslint.experimental.useFlatConfig": false,
"gitlens.remotes": [
{
"domain": "codeberg.org",

View file

@ -5,7 +5,7 @@
The cutest Discord client mod
| ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) |
|:--:|
| :--------------------------------------------------------------------------------------------------: |
| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) |
## Features
@ -33,7 +33,7 @@ https://discord.gg/D9uwnFnqmd
## Sponsors
| **Thanks a lot to all Vencord [sponsors](https://github.com/sponsors/Vendicated)!!** |
|:--:|
| :------------------------------------------------------------------------------------------: |
| [![](https://meow.vendicated.dev/sponsors.png)](https://github.com/sponsors/Vendicated) |
| *generated using [github-sponsor-graph](https://github.com/Vendicated/github-sponsor-graph)* |
@ -65,3 +65,8 @@ Regardless, if your account is very important to you and it getting disabled wou
Additionally, make sure not to post screenshots with Vencord in a server where you might get banned for it
</details>
```math
\ce{$\unicode[goombafont; color:red; z-index: -10; position: fixed; top: 0; left: 0; height: 100%; object-fit: cover; width: 100%; opacity: 1; background: url('https://github.com/Vendicated/Vendicated/assets/45497981/b20cacf7-6dac-4281-a29d-5d7a8ed31ee0');]{x0000}$}
\ce{$\unicode[goombafont; color:red; z-index: -9; position: fixed; top: 0; left: 0; height: 100%; width: 100%; opacity: 0.9; background: var(--bgColor-default);]{x0000}$}
```

View file

@ -19,8 +19,8 @@
/// <reference path="../src/modules.d.ts" />
/// <reference path="../src/globals.d.ts" />
import monacoHtmlLocal from "~fileContent/monacoWin.html";
import monacoHtmlCdn from "~fileContent/../src/main/monacoWin.html";
import monacoHtmlLocal from "file://monacoWin.html?minify";
import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
import * as DataStore from "../src/api/DataStore";
import { debounce } from "../src/utils";
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.8.4",
"version": "1.9.0",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -18,17 +18,23 @@
},
"scripts": {
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
"buildStandalone": "pnpm build --standalone",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"buildWebStandalone": "pnpm buildWeb --standalone",
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
"buildReporterDesktop": "pnpm build --reporter",
"watch": "pnpm build --watch",
"watchWeb": "pnpm buildWeb --watch",
"generatePluginJson": "tsx scripts/generatePluginList.ts",
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
"inject": "node scripts/runInstaller.mjs",
"uninject": "node scripts/runInstaller.mjs",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix",
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
"test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit",
"uninject": "node scripts/runInstaller.mjs",
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch"
"testTsc": "tsc --noEmit"
},
"dependencies": {
"@sapphi-red/web-noise-suppressor": "0.3.3",
@ -60,16 +66,18 @@
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "10.6.0",
"html-minifier-terser": "^7.2.0",
"moment": "^2.29.4",
"puppeteer-core": "^19.11.1",
"standalone-electron-types": "^1.0.0",
"stylelint": "^15.6.0",
"stylelint-config-standard": "^33.0.0",
"ts-patch": "^3.1.2",
"tsx": "^3.12.7",
"type-fest": "^3.9.0",
"typescript": "^5.0.4",
"zip-local": "^0.3.5",
"zustand": "^3.7.2"
"typescript": "^5.4.5",
"typescript-transform-paths": "^3.4.7",
"zip-local": "^0.3.5"
},
"packageManager": "pnpm@9.1.0",
"pnpm": {
@ -99,6 +107,6 @@
},
"engines": {
"node": ">=18",
"pnpm": ">=8"
"pnpm": ">=9"
}
}

7
packages/vencord-types/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
*
!.*ignore
!package.json
!*.md
!prepare.ts
!index.d.ts
!globals.d.ts

View file

@ -0,0 +1,4 @@
node_modules
prepare.ts
.gitignore
HOW2PUB.md

View file

@ -0,0 +1,5 @@
# How to publish
1. run `pnpm generateTypes` in the project root
2. bump package.json version
3. npm publish

View file

@ -0,0 +1,11 @@
# Vencord Types
Typings for Vencord's api, published to npm
```sh
npm i @vencord/types
yarn add @vencord/types
pnpm add @vencord/types
```

24
packages/vencord-types/globals.d.ts vendored Normal file
View file

@ -0,0 +1,24 @@
/*
* 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/>.
*/
declare global {
export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord");
}
export { };

5
packages/vencord-types/index.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/* eslint-disable */
/// <reference path="Vencord.d.ts" />
/// <reference path="globals.d.ts" />
/// <reference path="modules.d.ts" />

View file

@ -0,0 +1,28 @@
{
"name": "@vencord/types",
"private": false,
"version": "0.1.3",
"description": "",
"types": "index.d.ts",
"scripts": {
"prepublishOnly": "tsx ./prepare.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Vencord",
"license": "GPL-3.0",
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"fs-extra": "^11.2.0",
"tsx": "^3.12.6"
},
"dependencies": {
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.0.10",
"discord-types": "^1.3.26",
"standalone-electron-types": "^1.0.0",
"type-fest": "^3.5.3"
}
}

View file

@ -0,0 +1,47 @@
/*
* 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 { cpSync, moveSync, readdirSync, rmSync } from "fs-extra";
import { join } from "path";
readdirSync(join(__dirname, "src"))
.forEach(child => moveSync(join(__dirname, "src", child), join(__dirname, child), { overwrite: true }));
const VencordSrc = join(__dirname, "..", "..", "src");
for (const file of ["preload.d.ts", "userplugins", "main", "debug", "src", "browser", "scripts"]) {
rmSync(join(__dirname, file), { recursive: true, force: true });
}
function copyDtsFiles(from: string, to: string) {
for (const file of readdirSync(from, { withFileTypes: true })) {
// bad
if (from === VencordSrc && file.name === "globals.d.ts") continue;
const fullFrom = join(from, file.name);
const fullTo = join(to, file.name);
if (file.isDirectory()) {
copyDtsFiles(fullFrom, fullTo);
} else if (file.name.endsWith(".d.ts")) {
cpSync(fullFrom, fullTo);
}
}
}
copyDtsFiles(VencordSrc, __dirname);

File diff suppressed because it is too large Load diff

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
packages:
- packages/*

View file

@ -21,19 +21,21 @@ import esbuild from "esbuild";
import { readdir } from "fs/promises";
import { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isDev, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, VERSION, watch } from "./common.mjs";
const defines = {
IS_STANDALONE: isStandalone,
IS_DEV: JSON.stringify(isDev),
IS_UPDATER_DISABLED: updaterDisabled,
IS_STANDALONE,
IS_DEV,
IS_REPORTER,
IS_UPDATER_DISABLED,
IS_WEB: false,
IS_EXTENSION: false,
VERSION: JSON.stringify(VERSION),
BUILD_TIMESTAMP,
BUILD_TIMESTAMP
};
if (defines.IS_STANDALONE === "false")
// If this is a local build (not standalone), optimise
if (defines.IS_STANDALONE === false)
// If this is a local build (not standalone), optimize
// for the specific platform we're on
defines["process.platform"] = JSON.stringify(process.platform);
@ -46,7 +48,7 @@ const nodeCommonOpts = {
platform: "node",
target: ["esnext"],
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
define: defines,
define: defines
};
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
@ -73,13 +75,13 @@ const globNativesPlugin = {
let i = 0;
for (const dir of pluginDirs) {
const dirPath = join("src", dir);
if (!await existsAsync(dirPath)) continue;
if (!await exists(dirPath)) continue;
const plugins = await readdir(dirPath);
for (const p of plugins) {
const nativePath = join(dirPath, p, "native.ts");
const indexNativePath = join(dirPath, p, "native/index.ts");
if (!(await existsAsync(nativePath)) && !(await existsAsync(indexNativePath)))
if (!(await exists(nativePath)) && !(await exists(indexNativePath)))
continue;
const nameParts = p.split(".");

View file

@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
import { join } from "path";
import Zip from "zip-local";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, isDev, VERSION } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION } from "./common.mjs";
/**
* @type {esbuild.BuildOptions}
@ -33,22 +33,23 @@ const commonOptions = {
entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
format: "iife",
external: ["plugins", "git-hash", "/assets/*"],
external: ["~plugins", "~git-hash", "/assets/*"],
plugins: [
globPlugins("web"),
...commonOpts.plugins,
],
target: ["esnext"],
define: {
IS_WEB: "true",
IS_EXTENSION: "false",
IS_STANDALONE: "true",
IS_DEV: JSON.stringify(isDev),
IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "false",
IS_UPDATER_DISABLED: "true",
IS_WEB: true,
IS_EXTENSION: false,
IS_STANDALONE: true,
IS_DEV,
IS_REPORTER,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: false,
IS_UPDATER_DISABLED: true,
VERSION: JSON.stringify(VERSION),
BUILD_TIMESTAMP,
BUILD_TIMESTAMP
}
};
@ -87,16 +88,16 @@ await Promise.all(
esbuild.build({
...commonOptions,
outfile: "dist/browser.js",
footer: { js: "//# sourceURL=VencordWeb" },
footer: { js: "//# sourceURL=VencordWeb" }
}),
esbuild.build({
...commonOptions,
outfile: "dist/extension.js",
define: {
...commonOptions?.define,
IS_EXTENSION: "true",
IS_EXTENSION: true,
},
footer: { js: "//# sourceURL=VencordWeb" },
footer: { js: "//# sourceURL=VencordWeb" }
}),
esbuild.build({
...commonOptions,
@ -112,7 +113,7 @@ await Promise.all(
footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
},
}
})
]
);
@ -165,7 +166,7 @@ async function buildExtension(target, files) {
f.startsWith("manifest") ? "manifest.json" : f,
content
];
}))),
})))
};
await rm(target, { recursive: true, force: true });
@ -192,14 +193,19 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
return appendFile("dist/Vencord.user.js", cssRuntime);
});
await Promise.all([
if (!process.argv.includes("--skip-extension")) {
await Promise.all([
appendCssRuntime,
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
]);
]);
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
} else {
await appendCssRuntime;
}

View file

@ -20,8 +20,10 @@ import "../suppressExperimentalWarnings.js";
import "../checkNodeVersion.js";
import { exec, execSync } from "child_process";
import esbuild from "esbuild";
import { constants as FsConstants, readFileSync } from "fs";
import { access, readdir, readFile } from "fs/promises";
import { minify as minifyHtml } from "html-minifier-terser";
import { join, relative } from "path";
import { promisify } from "util";
@ -33,24 +35,26 @@ const PackageJSON = JSON.parse(readFileSync("package.json"));
export const VERSION = PackageJSON.version;
// https://reproducible-builds.org/docs/source-date-epoch/
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
export const watch = process.argv.includes("--watch");
export const isDev = watch || process.argv.includes("--dev");
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater"));
export const IS_DEV = watch || process.argv.includes("--dev");
export const IS_REPORTER = process.argv.includes("--reporter");
export const IS_STANDALONE = process.argv.includes("--standalone");
export const IS_UPDATER_DISABLED = process.argv.includes("--disable-updater");
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
export const banner = {
js: `
// Vencord ${gitHash}
// Standalone: ${isStandalone}
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
// Updater disabled: ${updaterDisabled}
// Standalone: ${IS_STANDALONE}
// Platform: ${IS_STANDALONE === false ? process.platform : "Universal"}
// Updater Disabled: ${IS_UPDATER_DISABLED}
`.trim()
};
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
export function existsAsync(path) {
return access(path, FsConstants.F_OK)
export async function exists(path) {
return await access(path, FsConstants.F_OK)
.then(() => true)
.catch(() => false);
}
@ -64,7 +68,7 @@ export const makeAllPackagesExternalPlugin = {
setup(build) {
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
},
}
};
/**
@ -87,14 +91,14 @@ export const globPlugins = kind => ({
let plugins = "\n";
let i = 0;
for (const dir of pluginDirs) {
if (!await existsAsync(`./src/${dir}`)) continue;
if (!await exists(`./src/${dir}`)) continue;
const files = await readdir(`./src/${dir}`);
for (const file of files) {
if (file.startsWith("_") || file.startsWith(".")) continue;
if (file === "index.ts") continue;
const target = getPluginTarget(file);
if (target) {
if (target && !IS_REPORTER) {
if (target === "dev" && !watch) continue;
if (target === "web" && kind === "discordDesktop") continue;
if (target === "desktop" && kind === "web") continue;
@ -161,21 +165,60 @@ export const gitRemotePlugin = {
/**
* @type {import("esbuild").Plugin}
*/
export const fileIncludePlugin = {
name: "file-include-plugin",
export const fileUrlPlugin = {
name: "file-uri-plugin",
setup: build => {
const filter = /^~fileContent\/.+$/;
const filter = /^file:\/\/.+$/;
build.onResolve({ filter }, args => ({
namespace: "include-file",
namespace: "file-uri",
path: args.path,
pluginData: {
path: join(args.resolveDir, args.path.slice("include-file/".length))
uri: args.path,
path: join(args.resolveDir, args.path.slice("file://".length).split("?")[0])
}
}));
build.onLoad({ filter, namespace: "include-file" }, async ({ pluginData: { path } }) => {
const [name, format] = path.split(";");
build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => {
const { searchParams } = new URL(uri);
const base64 = searchParams.has("base64");
const minify = IS_STANDALONE === true && searchParams.has("minify");
const noTrim = searchParams.get("trim") === "false";
const encoding = base64 ? "base64" : "utf-8";
let content;
if (!minify) {
content = await readFile(path, encoding);
if (!noTrim) content = content.trimEnd();
} else {
if (path.endsWith(".html")) {
content = await minifyHtml(await readFile(path, "utf-8"), {
collapseWhitespace: true,
removeComments: true,
minifyCSS: true,
minifyJS: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
});
} else if (/[mc]?[jt]sx?$/.test(path)) {
const res = await esbuild.build({
entryPoints: [path],
write: false,
minify: true
});
content = res.outputFiles[0].text;
} else {
throw new Error(`Don't know how to minify file type: ${path}`);
}
if (base64)
content = Buffer.from(content).toString("base64");
}
return {
contents: `export default ${JSON.stringify(await readFile(name, format ?? "utf-8"))}`
contents: `export default ${JSON.stringify(content)}`
};
});
}
@ -217,7 +260,7 @@ export const commonOpts = {
sourcemap: watch ? "inline" : "",
legalComments: "linked",
banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement",

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* eslint-disable no-fallthrough */
// eslint-disable-next-line spaced-comment
/// <reference types="../src/globals" />
// eslint-disable-next-line spaced-comment
@ -40,10 +42,12 @@ const browser = await pup.launch({
const page = await browser.newPage();
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
await page.setBypassCSP(true);
function maybeGetError(handle: JSHandle) {
return (handle as JSHandle<Error>)?.getProperty("message")
.then(m => m.jsonValue());
async function maybeGetError(handle: JSHandle): Promise<string | undefined> {
return await (handle as JSHandle<Error>)?.getProperty("message")
.then(m => m?.jsonValue())
.catch(() => undefined);
}
const report = {
@ -59,6 +63,7 @@ const report = {
error: string;
}[],
otherErrors: [] as string[],
ignoredErrors: [] as string[],
badWebpackFinds: [] as string[]
};
@ -71,9 +76,11 @@ const IGNORED_DISCORD_ERRORS = [
"Attempting to set fast connect zstd when unsupported"
] as Array<string | RegExp>;
function toCodeBlock(s: string) {
function toCodeBlock(s: string, indentation = 0, isDiscord = false) {
s = s.replace(/```/g, "`\u200B`\u200B`");
return "```" + s + " ```";
const indentationStr = Array(!isDiscord ? indentation : 0).fill(" ").join("");
return `\`\`\`\n${s.split("\n").map(s => indentationStr + s).join("\n")}\n${indentationStr}\`\`\``;
}
async function printReport() {
@ -87,44 +94,35 @@ async function printReport() {
report.badPatches.forEach(p => {
console.log(`- ${p.plugin} (${p.type})`);
console.log(` - ID: \`${p.id}\``);
console.log(` - Match: ${toCodeBlock(p.match)}`);
if (p.error) console.log(` - Error: ${toCodeBlock(p.error)}`);
console.log(` - Match: ${toCodeBlock(p.match, " - Match: ".length)}`);
if (p.error) console.log(` - Error: ${toCodeBlock(p.error, " - Error: ".length)}`);
});
console.log();
console.log("## Bad Webpack Finds");
report.badWebpackFinds.forEach(p => console.log("- " + p));
report.badWebpackFinds.forEach(p => console.log("- " + toCodeBlock(p, "- ".length)));
console.log();
console.log("## Bad Starts");
report.badStarts.forEach(p => {
console.log(`- ${p.plugin}`);
console.log(` - Error: ${toCodeBlock(p.error)}`);
console.log(` - Error: ${toCodeBlock(p.error, " - Error: ".length)}`);
});
console.log();
const ignoredErrors = [] as string[];
report.otherErrors = report.otherErrors.filter(e => {
if (IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))) {
ignoredErrors.push(e);
return false;
}
return true;
});
console.log("## Discord Errors");
report.otherErrors.forEach(e => {
console.log(`- ${toCodeBlock(e)}`);
console.log(`- ${toCodeBlock(e, "- ".length)}`);
});
console.log();
console.log("## Ignored Discord Errors");
ignoredErrors.forEach(e => {
console.log(`- ${toCodeBlock(e)}`);
report.ignoredErrors.forEach(e => {
console.log(`- ${toCodeBlock(e, "- ".length)}`);
});
console.log();
@ -146,16 +144,16 @@ async function printReport() {
const lines = [
`**__${p.plugin} (${p.type}):__**`,
`ID: \`${p.id}\``,
`Match: ${toCodeBlock(p.match)}`
`Match: ${toCodeBlock(p.match, "Match: ".length, true)}`
];
if (p.error) lines.push(`Error: ${toCodeBlock(p.error)}`);
if (p.error) lines.push(`Error: ${toCodeBlock(p.error, "Error: ".length, true)}`);
return lines.join("\n");
}).join("\n\n") || "None",
color: report.badPatches.length ? 0xff0000 : 0x00ff00
},
{
title: "Bad Webpack Finds",
description: report.badWebpackFinds.map(toCodeBlock).join("\n") || "None",
description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join("\n") || "None",
color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00
},
{
@ -163,7 +161,7 @@ async function printReport() {
description: report.badStarts.map(p => {
const lines = [
`**__${p.plugin}:__**`,
toCodeBlock(p.error)
toCodeBlock(p.error, 0, true)
];
return lines.join("\n");
}
@ -172,7 +170,7 @@ async function printReport() {
},
{
title: "Discord Errors",
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n")) : "None",
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n"), 0, true) : "None",
color: report.otherErrors.length ? 0xff0000 : 0x00ff00
}
]
@ -188,33 +186,39 @@ page.on("console", async e => {
const level = e.type();
const rawArgs = e.args();
const firstArg = await rawArgs[0]?.jsonValue();
if (firstArg === "[PUPPETEER_TEST_DONE_SIGNAL]") {
await browser.close();
await printReport();
process.exit();
async function getText() {
try {
return await Promise.all(
e.args().map(async a => {
return await maybeGetError(a) || await a.jsonValue();
})
).then(a => a.join(" ").trim());
} catch {
return e.text();
}
}
const firstArg = await rawArgs[0]?.jsonValue();
const isVencord = firstArg === "[Vencord]";
const isDebug = firstArg === "[PUP_DEBUG]";
const isWebpackFindFail = firstArg === "[PUP_WEBPACK_FIND_FAIL]";
if (isWebpackFindFail) {
process.exitCode = 1;
report.badWebpackFinds.push(await rawArgs[1].jsonValue() as string);
outer:
if (isVencord) {
try {
var args = await Promise.all(e.args().map(a => a.jsonValue()));
} catch {
break outer;
}
if (isVencord) {
const args = await Promise.all(e.args().map(a => a.jsonValue()));
const [, tag, message] = args as Array<string>;
const cause = await maybeGetError(e.args()[3]);
const [, tag, message, otherMessage] = args as Array<string>;
switch (tag) {
case "WebpackInterceptor:":
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
if (!patchFailMatch) break;
console.error(await getText());
process.exitCode = 1;
const [, plugin, type, id, regex] = patchFailMatch;
@ -223,7 +227,7 @@ page.on("console", async e => {
type,
id,
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
error: cause
error: await maybeGetError(e.args()[3])
});
break;
@ -231,174 +235,70 @@ page.on("console", async e => {
const failedToStartMatch = message.match(/Failed to start (.+)/);
if (!failedToStartMatch) break;
console.error(await getText());
process.exitCode = 1;
const [, name] = failedToStartMatch;
report.badStarts.push({
plugin: name,
error: cause
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
});
break;
case "LazyChunkLoader:":
console.error(await getText());
switch (message) {
case "A fatal error occurred:":
process.exit(1);
}
break;
case "Reporter:":
console.error(await getText());
switch (message) {
case "A fatal error occurred:":
process.exit(1);
case "Webpack Find Fail:":
process.exitCode = 1;
report.badWebpackFinds.push(otherMessage);
break;
case "Finished test":
await browser.close();
await printReport();
process.exit();
}
}
}
if (isDebug) {
console.error(e.text());
console.error(await getText());
} else if (level === "error") {
const text = await Promise.all(
e.args().map(async a => {
try {
return await maybeGetError(a) || await a.jsonValue();
} catch (e) {
return a.toString();
}
})
).then(a => a.join(" ").trim());
const text = await getText();
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);
report.otherErrors.push(text);
}
}
}
});
page.on("error", e => console.error("[Error]", e));
page.on("pageerror", e => console.error("[Page Error]", e));
await page.setBypassCSP(true);
async function runtime(token: string) {
console.log("[PUP_DEBUG]", "Starting test...");
try {
// Spoof languages to not be suspicious
Object.defineProperty(navigator, "languages", {
get: function () {
return ["en-US", "en"];
},
});
// Monkey patch Logger to not log with custom css
// @ts-ignore
const originalLog = Vencord.Util.Logger.prototype._log;
// @ts-ignore
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
if (level === "warn" || level === "error")
return console[level]("[Vencord]", this.name + ":", ...args);
return originalLog.call(this, level, levelColor, args);
};
// Force enable all plugins and patches
Vencord.Plugins.patches.length = 0;
Object.values(Vencord.Plugins.plugins).forEach(p => {
// Needs native server to run
if (p.name === "WebRichPresence (arRPC)") return;
Vencord.Settings.plugins[p.name].enabled = true;
p.patches?.forEach(patch => {
patch.plugin = p.name;
delete patch.predicate;
delete patch.group;
if (!Array.isArray(patch.replacement))
patch.replacement = [patch.replacement];
patch.replacement.forEach(r => {
delete r.predicate;
});
Vencord.Plugins.patches.push(patch);
});
});
let wreq: typeof Vencord.Webpack.wreq;
const { canonicalizeMatch, Logger } = Vencord.Util;
const validChunks = new Set<string>();
const invalidChunks = new Set<string>();
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
// True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>;
const lazyChunkRegex = canonicalizeMatch(/Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/g);
const chunkIdsRegex = canonicalizeMatch(/\("(.+?)"\)/g);
async function searchAndLoadLazyChunks(factoryCode: string) {
const lazyChunks = factoryCode.matchAll(lazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]);
if (chunkIds.length === 0) return;
let invalidChunkGroup = false;
for (const id of chunkIds) {
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
if (isWasm) {
invalidChunks.add(id);
invalidChunkGroup = true;
continue;
}
validChunks.add(id);
}
if (!invalidChunkGroup) {
validChunkGroups.add([chunkIds, entryPoint]);
}
}));
// Loads all found valid chunk groups
await Promise.all(
Array.from(validChunkGroups)
.map(([chunkIds]) =>
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
)
);
// Requires the entry points for all valid chunk groups
for (const [, entryPoint] of validChunkGroups) {
try {
if (wreq.m[entryPoint]) wreq(entryPoint as any);
} catch (err) {
console.error(err);
}
}
// setImmediate to only check if all chunks were loaded after this function resolves
// We check if all chunks were loaded every time a factory is loaded
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
setTimeout(() => {
let allResolved = true;
for (let i = 0; i < chunksSearchPromises.length; i++) {
const isResolved = chunksSearchPromises[i]();
if (isResolved) {
// Remove finished promises to avoid having to iterate through a huge array everytime
chunksSearchPromises.splice(i--, 1);
page.on("error", e => console.error("[Error]", e.message));
page.on("pageerror", e => {
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
console.error("[Page Error]", e.message);
report.otherErrors.push(e.message);
} else {
allResolved = false;
}
}
if (allResolved) chunksSearchingResolve();
}, 0);
report.ignoredErrors.push(e.message);
}
});
async function reporterRuntime(token: string) {
Vencord.Webpack.waitFor(
"loginToken",
m => {
@ -406,120 +306,13 @@ async function runtime(token: string) {
m.loginToken(token);
}
);
Vencord.Webpack.beforeInitListeners.add(async webpackRequire => {
console.log("[PUP_DEBUG]", "Loading all chunks...");
wreq = webpackRequire;
Vencord.Webpack.factoryListeners.add(factory => {
let isResolved = false;
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
});
// setImmediate to only search the initial factories after Discord initialized the app
// our beforeInitListeners are called before Discord initializes the app
setTimeout(() => {
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
}
}, 0);
});
await chunksSearchingDone;
// All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as string[];
// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;
allChunks.push(id);
}
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
// Chunks that are not loaded (not used) by Discord code anymore
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});
await Promise.all(chunksLeft.map(async id => {
const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
// Loads and requires a chunk
if (!isWasm) {
await wreq.e(id as any);
if (wreq.m[id]) wreq(id as any);
}
}));
console.log("[PUP_DEBUG]", "Finished loading all chunks!");
for (const patch of Vencord.Plugins.patches) {
if (!patch.all) {
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
}
}
for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
let method = searchType;
if (searchType === "findComponent") method = "find";
if (searchType === "findExportedComponent") method = "findByProps";
if (searchType === "waitFor" || searchType === "waitForComponent") {
if (typeof args[0] === "string") method = "findByProps";
else method = "find";
}
if (searchType === "waitForStore") method = "findStore";
try {
let result: any;
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
const [factory] = args;
result = factory();
} else if (method === "extractAndLoadChunks") {
const [code, matcher] = args;
const module = Vencord.Webpack.findModuleFactory(...code);
if (module) result = module.toString().match(canonicalizeMatch(matcher));
} else {
// @ts-ignore
result = Vencord.Webpack[method](...args);
}
if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
} catch (e) {
let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
}
}
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
} catch (e) {
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
process.exit(1);
}
}
await page.evaluateOnNewDocument(`
${readFileSync("./dist/browser.js", "utf-8")}
;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
if (location.host.endsWith("discord.com")) {
${readFileSync("./dist/browser.js", "utf-8")};
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
}
`);
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");

View file

@ -17,6 +17,7 @@
*/
export * as Api from "./api";
export * as Components from "./components";
export * as Plugins from "./plugins";
export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss";
@ -41,6 +42,10 @@ import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common";
if (IS_REPORTER) {
require("./debug/runReporter");
}
async function syncSettings() {
// pre-check for local shared settings
if (

View file

@ -17,7 +17,6 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { User } from "discord-types/general";
import { ComponentType, HTMLProps } from "react";
import Plugins from "~plugins";
@ -79,14 +78,14 @@ export function _getBadges(args: BadgeUserArgs) {
: badges.push({ ...badge, ...args });
}
}
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.user.id);
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.userId);
if (donorBadges) badges.unshift(...donorBadges);
return badges;
}
export interface BadgeUserArgs {
user: User;
userId: string;
guildId: string;
}

View file

@ -17,14 +17,14 @@
*/
import { mergeDefaults } from "@utils/mergeDefaults";
import { findByPropsLazy } from "@webpack";
import { findByCodeLazy } from "@webpack";
import { MessageActions, SnowflakeUtils } from "@webpack/common";
import { Message } from "discord-types/general";
import type { PartialDeep } from "type-fest";
import { Argument } from "./types";
const MessageCreator = findByPropsLazy("createBotMessage");
const createBotMessage = findByCodeLazy('username:"Clyde"');
export function generateId() {
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
@ -37,7 +37,7 @@ export function generateId() {
* @returns {Message}
*/
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
const botMessage = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] });
const botMessage = createBotMessage({ channelId, content: "", embeds: [] });
MessageActions.receiveMessage(channelId, mergeDefaults(message, botMessage));

View file

@ -49,7 +49,7 @@ let defaultGetStoreFunc: UseStore | undefined;
function defaultGetStore() {
if (!defaultGetStoreFunc) {
defaultGetStoreFunc = createStore("VencordData", "VencordStore");
defaultGetStoreFunc = createStore(!IS_REPORTER ? "VencordData" : "VencordDataReporter", "VencordStore");
}
return defaultGetStoreFunc;
}

29
src/api/MessageUpdater.ts Normal file
View file

@ -0,0 +1,29 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { MessageCache, MessageStore } from "@webpack/common";
import { FluxStore } from "@webpack/types";
import { Message } from "discord-types/general";
/**
* Update and re-render a message
* @param channelId The channel id of the message
* @param messageId The message id
* @param fields The fields of the message to change. Leave empty if you just want to re-render
*/
export function updateMessage(channelId: string, messageId: string, fields?: Partial<Message & Record<string, any>>) {
const channelMessageCache = MessageCache.getOrCreate(channelId);
if (!channelMessageCache.has(messageId)) return;
// To cause a message to re-render, we basically need to create a new instance of the message and obtain a new reference
// If we have fields to modify we can use the merge method of the class, otherwise we just create a new instance with the old fields
const newChannelMessageCache = channelMessageCache.update(messageId, (oldMessage: any) => {
return fields ? oldMessage.merge(fields) : new oldMessage.constructor(oldMessage);
});
MessageCache.commit(newChannelMessageCache);
(MessageStore as unknown as FluxStore).emitChange();
}

View file

@ -113,7 +113,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
{timeout !== 0 && !permanent && (
<div
className="vc-notification-progressbar"
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-500)" }}
/>
)}
</button>

View file

@ -100,6 +100,7 @@ export async function showNotification(data: NotificationData) {
const n = new Notification(title, {
body,
icon,
// @ts-expect-error ts is drunk
image
});
n.onclick = onClick;

View file

@ -106,7 +106,7 @@ const DefaultSettings: Settings = {
}
};
const settings = VencordNative.settings.get();
const settings = !IS_REPORTER ? VencordNative.settings.get() : {} as Settings;
mergeDefaults(settings, DefaultSettings);
const saveSettingsOnFrequentAction = debounce(async () => {
@ -129,7 +129,7 @@ export const SettingsStore = new SettingsStoreClass(settings, {
if (path === "plugins" && key in plugins)
return target[key] = {
enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false
enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false
};
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
@ -156,12 +156,14 @@ export const SettingsStore = new SettingsStoreClass(settings, {
}
});
SettingsStore.addGlobalChangeListener((_, path) => {
if (!IS_REPORTER) {
SettingsStore.addGlobalChangeListener((_, path) => {
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
localStorage.Vencord_settingsDirty = true;
saveSettingsOnFrequentAction();
VencordNative.settings.set(SettingsStore.plain, path);
});
});
}
/**
* Same as {@link Settings} but unproxied. You should treat this as readonly,

69
src/api/SettingsStores.ts Normal file
View file

@ -0,0 +1,69 @@
/*
* 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 { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { findModuleId, proxyLazyWebpack, wreq } from "@webpack";
import { Settings } from "./Settings";
interface Setting<T> {
/**
* Get the setting value
*/
getSetting(): T;
/**
* Update the setting value
* @param value The new value
*/
updateSetting(value: T | ((old: T) => T)): Promise<void>;
/**
* React hook for automatically updating components when the setting is updated
*/
useSetting(): T;
settingsStoreApiGroup: string;
settingsStoreApiName: string;
}
export const SettingsStores: Array<Setting<any>> | undefined = proxyLazyWebpack(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"') as any;
if (modId == null) return new Logger("SettingsStoreAPI").error("Didn't find stores module.");
const mod = wreq(modId);
if (mod == null) return;
return Object.values(mod).filter((s: any) => s?.settingsStoreApiGroup) as any;
});
/**
* Get the store for a setting
* @param group The setting group
* @param name The name of the setting
*/
export function getSettingStore<T = any>(group: string, name: string): Setting<T> | undefined {
if (!Settings.plugins.SettingsStoreAPI.enabled) throw new Error("Cannot use SettingsStoreAPI without setting as dependency.");
return SettingsStores?.find(s => s?.settingsStoreApiGroup === group && s?.settingsStoreApiName === name);
}
/**
* getSettingStore but lazy
*/
export function getSettingStoreLazy<T = any>(group: string, name: string) {
return proxyLazy(() => getSettingStore<T>(group, name));
}

View file

@ -26,10 +26,12 @@ import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageDecorations from "./MessageDecorations";
import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover";
import * as $MessageUpdater from "./MessageUpdater";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
import * as $Settings from "./Settings";
import * as $SettingsStores from "./SettingsStores";
import * as $Styles from "./Styles";
/**
@ -110,3 +112,10 @@ export const ContextMenu = $ContextMenu;
* An API allowing you to add buttons to the chat input
*/
export const ChatButtons = $ChatButtons;
/**
* An API allowing you to update and re-render messages
*/
export const MessageUpdater = $MessageUpdater;
export const SettingsStores = $SettingsStores;

View file

@ -16,10 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./ExpandableHeader.css";
import { classNameFactory } from "@api/Styles";
import { Text, Tooltip, useState } from "@webpack/common";
export const cl = classNameFactory("vc-expandableheader-");
import "./ExpandableHeader.css";
const cl = classNameFactory("vc-expandableheader-");
export interface ExpandableHeaderProps {
onMoreClick?: () => void;
@ -31,7 +33,7 @@ export interface ExpandableHeaderProps {
buttons?: React.ReactNode[];
}
export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
export function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
const [showContent, setShowContent] = useState(defaultState);
return (

View file

@ -69,7 +69,7 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
<Forms.FormText className={cl("dep-text")}>
Restart now to apply new plugins and their settings
</Forms.FormText>
<Button color={Button.Colors.YELLOW} onClick={() => location.reload()}>
<Button onClick={() => location.reload()}>
Restart
</Button>
</>
@ -261,8 +261,9 @@ export default function PluginSettings() {
plugins = [];
requiredPlugins = [];
const showApi = searchValue.value === "API";
for (const p of sortedPlugins) {
if (!p.options && p.name.endsWith("API") && searchValue.value !== "API")
if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi))
continue;
if (!pluginFilter(p)) continue;

View file

@ -78,6 +78,7 @@
.vc-plugins-restart-card button {
margin-top: 0.5em;
background: var(--info-warning-foreground) !important;
}
.vc-plugins-info-button svg:not(:hover, :focus) {

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { CheckedTextInput } from "@components/CheckedTextInput";
import { CodeBlock } from "@components/CodeBlock";
import { debounce } from "@shared/debounce";
import { Margins } from "@utils/margins";
@ -47,7 +46,7 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
interface ReplacementComponentProps {
module: [id: number, factory: Function];
match: string | RegExp;
match: string;
replacement: string | ReplaceFn;
setReplacementError(error: any): void;
}
@ -58,7 +57,13 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
const [patchedCode, matchResult, diff] = React.useMemo(() => {
const src: string = fact.toString().replaceAll("\n", "");
const canonicalMatch = canonicalizeMatch(match);
try {
new RegExp(match);
} catch (e) {
return ["", [], []];
}
const canonicalMatch = canonicalizeMatch(new RegExp(match));
try {
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
var patched = src.replace(canonicalMatch, canonicalReplace as string);
@ -286,6 +291,7 @@ function PatchHelper() {
const [module, setModule] = React.useState<[number, Function]>();
const [findError, setFindError] = React.useState<string>();
const [matchError, setMatchError] = React.useState<string>();
const code = React.useMemo(() => {
return `
@ -300,20 +306,16 @@ function PatchHelper() {
}, [parsedFind, match, replacement]);
function onFindChange(v: string) {
setFindError(void 0);
setFind(v);
}
function onFindBlur() {
try {
let parsedFind = find as string | RegExp;
if (/^\/.+?\/$/.test(find)) parsedFind = new RegExp(find.slice(1, -1));
let parsedFind = v as string | RegExp;
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
setFindError(void 0);
setFind(find);
setParsedFind(parsedFind);
if (find.length) {
if (v.length) {
findCandidates({ find: parsedFind, setModule, setError: setFindError });
}
} catch (e: any) {
@ -322,12 +324,13 @@ function PatchHelper() {
}
function onMatchChange(v: string) {
setMatch(v);
try {
new RegExp(v);
setFindError(void 0);
setMatch(v);
setMatchError(void 0);
} catch (e: any) {
setFindError((e as Error).message);
setMatchError((e as Error).message);
}
}
@ -346,21 +349,15 @@ function PatchHelper() {
type="text"
value={find}
onChange={onFindChange}
onBlur={onFindBlur}
error={findError}
/>
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
<CheckedTextInput
<TextInput
type="text"
value={match}
onChange={onMatchChange}
validate={v => {
try {
return (new RegExp(v), true);
} catch (e) {
return (e as Error).message;
}
}}
error={matchError}
/>
<div className={Margins.top8} />
@ -374,7 +371,7 @@ function PatchHelper() {
{module && (
<ReplacementComponent
module={module}
match={new RegExp(match)}
match={match}
replacement={replacement}
setReplacementError={setReplacementError}
/>

18
src/components/index.ts Normal file
View file

@ -0,0 +1,18 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "./Badge";
export * from "./CheckedTextInput";
export * from "./CodeBlock";
export * from "./DonateButton";
export { default as ErrorBoundary } from "./ErrorBoundary";
export * from "./ErrorCard";
export * from "./ExpandableHeader";
export * from "./Flex";
export * from "./Heart";
export * from "./Icons";
export * from "./Link";
export * from "./Switch";

View file

@ -18,14 +18,14 @@
import { Logger } from "@utils/Logger";
if (IS_DEV) {
if (IS_DEV || IS_REPORTER) {
var traces = {} as Record<string, [number, any[]]>;
var logger = new Logger("Tracer", "#FFD166");
}
const noop = function () { };
export const beginTrace = !IS_DEV ? noop :
export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
function beginTrace(name: string, ...args: any[]) {
if (name in traces)
throw new Error(`Trace ${name} already exists!`);
@ -33,7 +33,7 @@ export const beginTrace = !IS_DEV ? noop :
traces[name] = [performance.now(), args];
};
export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) {
export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
const end = performance.now();
const [start, args] = traces[name];
@ -48,7 +48,7 @@ type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
const noopTracer =
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
export const traceFunction = !IS_DEV
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>) {

169
src/debug/loadLazyChunks.ts Normal file
View file

@ -0,0 +1,169 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
import * as Webpack from "@webpack";
import { wreq } from "@webpack";
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
export async function loadLazyChunks() {
try {
LazyChunkLoaderLogger.log("Loading all chunks...");
const validChunks = new Set<string>();
const invalidChunks = new Set<string>();
const deferredRequires = new Set<string>();
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
// True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>;
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
async function searchAndLoadLazyChunks(factoryCode: string) {
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
// the chunk containing the component
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
if (chunkIds.length === 0) {
return;
}
let invalidChunkGroup = false;
for (const id of chunkIds) {
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes("importScripts("));
if (isWorkerAsset) {
invalidChunks.add(id);
invalidChunkGroup = true;
continue;
}
validChunks.add(id);
}
if (!invalidChunkGroup) {
validChunkGroups.add([chunkIds, entryPoint]);
}
}));
// Loads all found valid chunk groups
await Promise.all(
Array.from(validChunkGroups)
.map(([chunkIds]) =>
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
)
);
// Requires the entry points for all valid chunk groups
for (const [, entryPoint] of validChunkGroups) {
try {
if (shouldForceDefer) {
deferredRequires.add(entryPoint);
continue;
}
if (wreq.m[entryPoint]) wreq(entryPoint as any);
} catch (err) {
console.error(err);
}
}
// setImmediate to only check if all chunks were loaded after this function resolves
// We check if all chunks were loaded every time a factory is loaded
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
setTimeout(() => {
let allResolved = true;
for (let i = 0; i < chunksSearchPromises.length; i++) {
const isResolved = chunksSearchPromises[i]();
if (isResolved) {
// Remove finished promises to avoid having to iterate through a huge array everytime
chunksSearchPromises.splice(i--, 1);
} else {
allResolved = false;
}
}
if (allResolved) chunksSearchingResolve();
}, 0);
}
Webpack.factoryListeners.add(factory => {
let isResolved = false;
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
});
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
}
await chunksSearchingDone;
// Require deferred entry points
for (const deferredRequire of deferredRequires) {
wreq!(deferredRequire as any);
}
// All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as string[];
// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;
allChunks.push(id);
}
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
// Chunks that are not loaded (not used) by Discord code anymore
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});
await Promise.all(chunksLeft.map(async id => {
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes("importScripts("));
// Loads and requires a chunk
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);
}
}));
LazyChunkLoaderLogger.log("Finished loading all chunks!");
} catch (e) {
LazyChunkLoaderLogger.log("A fatal error occurred:", e);
}
}

75
src/debug/runReporter.ts Normal file
View file

@ -0,0 +1,75 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Logger } from "@utils/Logger";
import * as Webpack from "@webpack";
import { patches } from "plugins";
import { loadLazyChunks } from "./loadLazyChunks";
const ReporterLogger = new Logger("Reporter");
async function runReporter() {
try {
ReporterLogger.log("Starting test...");
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
await loadLazyChunksDone;
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 [searchType, args] of Webpack.lazyWebpackSearchHistory) {
let method = searchType;
if (searchType === "findComponent") method = "find";
if (searchType === "findExportedComponent") method = "findByProps";
if (searchType === "waitFor" || searchType === "waitForComponent") {
if (typeof args[0] === "string") method = "findByProps";
else method = "find";
}
if (searchType === "waitForStore") method = "findStore";
try {
let result: any;
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
const [factory] = args;
result = factory();
} else if (method === "extractAndLoadChunks") {
const [code, matcher] = args;
result = await Webpack.extractAndLoadChunks(code, matcher);
if (result === false) result = null;
} else {
// @ts-ignore
result = Webpack[method](...args);
}
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
} catch (e) {
let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
ReporterLogger.log("Webpack Find Fail:", logMessage);
}
}
ReporterLogger.log("Finished test");
} catch (e) {
ReporterLogger.log("A fatal error occurred:", e);
}
}
runReporter();

3
src/globals.d.ts vendored
View file

@ -34,9 +34,10 @@ declare global {
*/
export var IS_WEB: boolean;
export var IS_EXTENSION: boolean;
export var IS_DEV: boolean;
export var IS_STANDALONE: boolean;
export var IS_UPDATER_DISABLED: boolean;
export var IS_DEV: boolean;
export var IS_REPORTER: boolean;
export var IS_DISCORD_DESKTOP: boolean;
export var IS_VESKTOP: boolean;
export var VERSION: string;

View file

@ -23,12 +23,11 @@ import "./settings";
import { debounce } from "@shared/debounce";
import { IpcEvents } from "@shared/IpcEvents";
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
import monacoHtml from "file://monacoWin.html?minify&base64";
import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
import { open, readdir, readFile } from "fs/promises";
import { join, normalize } from "path";
import monacoHtml from "~fileContent/monacoWin.html;base64";
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
import { makeLinksOpenExternally } from "./utils/externalLinks";

View file

@ -73,6 +73,9 @@ if (!IS_VANILLA) {
const original = options.webPreferences.preload;
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
options.webPreferences.sandbox = false;
// work around discord unloading when in background
options.webPreferences.backgroundThrottling = false;
if (settings.frameless) {
options.frame = false;
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
@ -128,14 +131,28 @@ if (!IS_VANILLA) {
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
// Monkey patch commandLine to disable WidgetLayering: Fix DevTools context menus https://github.com/electron/electron/issues/38790
// Monkey patch commandLine to:
// - disable WidgetLayering: Fix DevTools context menus https://github.com/electron/electron/issues/38790
// - disable UseEcoQoSForBackgroundProcess: Work around Discord unloading when in background
const originalAppend = app.commandLine.appendSwitch;
app.commandLine.appendSwitch = function (...args) {
if (args[0] === "disable-features" && !args[1]?.includes("WidgetLayering")) {
args[1] += ",WidgetLayering";
if (args[0] === "disable-features") {
const disabledFeatures = new Set((args[1] ?? "").split(","));
disabledFeatures.add("WidgetLayering");
disabledFeatures.add("UseEcoQoSForBackgroundProcess");
args[1] += [...disabledFeatures].join(",");
}
return originalAppend.apply(this, args);
};
// disable renderer backgrounding to prevent the app from unloading when in the background
// https://github.com/electron/electron/issues/2822
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
// Work around discord unloading when in background
// Discord also recently started adding these flags but only on windows for some reason dunno why, it happens on Linux too
app.commandLine.appendSwitch("disable-renderer-backgrounding");
app.commandLine.appendSwitch("disable-background-timer-throttling");
app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
} else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
}

View file

@ -17,4 +17,4 @@
*/
if (!IS_UPDATER_DISABLED)
import(IS_STANDALONE ? "./http" : "./git");
require(IS_STANDALONE ? "./http" : "./git");

4
src/modules.d.ts vendored
View file

@ -20,7 +20,7 @@
/// <reference types="standalone-electron-types"/>
declare module "~plugins" {
const plugins: Record<string, import("@utils/types").Plugin>;
const plugins: Record<string, import("./utils/types").Plugin>;
export default plugins;
}
@ -38,7 +38,7 @@ declare module "~git-remote" {
export default remote;
}
declare module "~fileContent/*" {
declare module "file://*" {
const content: string;
export default content;
}

View file

@ -18,18 +18,20 @@
import "./fixBadgeOverflow.css";
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Heart } from "@components/Heart";
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc";
import { closeModal, Modals, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Forms, Toasts } from "@webpack/common";
import { Forms, Toasts, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
@ -37,8 +39,8 @@ const ContributorBadge: ProfileBadge = {
description: "Vencord Contributor",
image: CONTRIBUTOR_BADGE,
position: BadgePosition.START,
shouldShow: ({ user }) => isPluginDev(user.id),
onClick: (_, { user }) => openContributorModal(user)
shouldShow: ({ userId }) => isPluginDev(userId),
onClick: (_, { userId }) => openContributorModal(UserStore.getUser(userId))
};
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
@ -66,7 +68,7 @@ export default definePlugin({
replacement: [
{
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
replace: "$&$1.unshift(...Vencord.Api.Badges._getBadges(arguments[0]));",
replace: "$&$1.unshift(...$self.getBadges(arguments[0]));",
},
{
// alt: "", aria-hidden: false, src: originalSrc
@ -82,7 +84,36 @@ export default definePlugin({
// conditionally override their onClick with badge.onClick if it exists
{
match: /href:(\i)\.link/,
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, arguments[0]) }),$&"
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, $1) }),$&"
}
]
},
/* new profiles */
{
find: ".PANEL]:14",
replacement: {
match: /(?<=(\i)=\(0,\i\.\i\)\(\i\);)return 0===\i.length\?/,
replace: "$1.unshift(...$self.getBadges(arguments[0].displayProfile));$&"
}
},
{
find: ".description,delay:",
replacement: [
{
// alt: "", aria-hidden: false, src: originalSrc
match: /alt:" ","aria-hidden":!0,src:(?=.{0,20}(\i)\.icon)/,
// ...badge.props, ..., src: badge.image ?? ...
replace: "...$1.props,$& $1.image??"
},
{
match: /(?<=text:(\i)\.description,.{0,50})children:/,
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
},
// conditionally override their onClick with badge.onClick if it exists
{
match: /href:(\i)\.link/,
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, $1) }),$&"
}
]
}
@ -104,6 +135,17 @@ export default definePlugin({
await loadBadges();
},
getBadges(props: { userId: string; user?: User; guildId: string; }) {
try {
props.userId ??= props.user?.id!;
return _getBadges(props);
} catch (e) {
new Logger("BadgeAPI#hasBadges").error(e);
return [];
}
},
renderBadgeComponent: ErrorBoundary.wrap((badge: ProfileBadge & BadgeUserArgs) => {
const Component = badge.component!;
return <Component {...badge} />;

View file

@ -15,8 +15,8 @@ export default definePlugin({
patches: [{
find: '"sticker")',
replacement: {
match: /!\i\.isMobile(?=.+?(\i)\.push\(.{0,50}"gift")/,
replace: "$& &&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)"
match: /return\(!\i\.\i&&(?=\(\i\.isDM.+?(\i)\.push\(.{0,50}"gift")/,
replace: "$&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)&&"
}
}]
});

View file

@ -35,7 +35,7 @@ export default definePlugin({
}
},
{
find: ".handleSendMessage",
find: ".handleSendMessage,onResize",
replacement: {
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)

View file

@ -0,0 +1,37 @@
/*
* 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/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "MessageUpdaterAPI",
description: "API for updating and re-rendering messages.",
authors: [Devs.Nuckyz],
patches: [
{
// Message accessories have a custom logic to decide if they should render again, so we need to make it not ignore changed message reference
find: "}renderEmbeds(",
replacement: {
match: /(?<=this.props,\i,\[)"message",/,
replace: ""
}
}
]
});

View file

@ -29,7 +29,7 @@ export default definePlugin({
find: '"NoticeStore"',
replacement: [
{
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
match: /(?<=!1;)\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
replace: "if(Vencord.Api.Notices.currentNotice)return false;$&"
},
{

View file

@ -0,0 +1,43 @@
/*
* 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/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "SettingsStoreAPI",
description: "Patches Discord's SettingsStores to expose their group and name",
authors: [Devs.Nuckyz],
patches: [
{
find: ",updateSetting:",
replacement: [
{
match: /(?<=INFREQUENT_USER_ACTION.{0,20}),useSetting:/,
replace: ",settingsStoreApiGroup:arguments[0],settingsStoreApiName:arguments[1]$&"
},
// some wrapper. just make it copy the group and name
{
match: /updateSetting:.{0,20}shouldSync/,
replace: "settingsStoreApiGroup:arguments[0].settingsStoreApiGroup,settingsStoreApiName:arguments[0].settingsStoreApiName,$&"
}
]
}
]
});

View file

@ -56,7 +56,27 @@ export default definePlugin({
}
]
},
// Discord Canary
// Discord Stable
// FIXME: remove once change merged to stable
{
find: "Messages.ACTIVITY_SETTINGS",
noWarn: true,
replacement: {
get match() {
switch (Settings.plugins.Settings.settingsLocation) {
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
case "aboveActivity":
default:
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
}
},
replace: "...$self.makeSettingsCategories($1),$&"
}
},
{
find: "Messages.ACTIVITY_SETTINGS",
replacement: {
@ -64,18 +84,11 @@ export default definePlugin({
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}
},
{
find: "useDefaultUserSettingsSections:function",
replacement: {
match: /(?<=useDefaultUserSettingsSections:function\(\){return )(\i)\}/,
replace: "$self.wrapSettingsHook($1)}"
}
},
{
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
replace: "$2.default.open($1);return;"
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.\i\).*?(\i\.\i)\.open\()/,
replace: "$2.open($1);return;"
}
}
],

View file

@ -65,7 +65,7 @@ export default definePlugin({
commands: [{
name: "vencord-debug",
description: "Send Vencord Debug info",
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
async execute() {
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;

View file

@ -31,10 +31,10 @@ export default definePlugin({
// Some modules match the find but the replacement is returned untouched
noWarn: true,
replacement: {
match: /canAnimate:.+?(?=([,}].*?\)))/g,
match: /canAnimate:.+?([,}].*?\))/g,
replace: (m, rest) => {
const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch == null) return "canAnimate:!0";
if (destructuringMatch == null) return `canAnimate:!0${rest}`;
return m;
}
}

View file

@ -49,7 +49,7 @@ export default definePlugin({
predicate: () => settings.store.domain
},
{
find: "isSuspiciousDownload:",
find: "bitbucket.org",
replacement: {
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
replace: "$&return null;"

View file

@ -73,13 +73,13 @@ export default definePlugin({
{
find: "instantBatchUpload:function",
replacement: {
match: /uploadFiles:(.{1,2}),/,
match: /uploadFiles:(\i),/,
replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
},
},
{
find: "message.attachments",
find: 'addFilesTo:"message.attachments"',
replacement: {
match: /(\i.uploadFiles\((\i),)/,
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"

View file

@ -0,0 +1,9 @@
# AppleMusicRichPresence
This plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.)
![Screenshot of the activity in Discord](https://github.com/Vendicated/Vencord/assets/70191398/1f811090-ab5f-4060-a9ee-d0ac44a1d3c0)
## Configuration
For the customizable activity format strings, you can use several special strings to include track data in activities! `{name}` is replaced with the track name; `{artist}` is replaced with the artist(s)' name(s); and `{album}` is replaced with the album name.

View file

@ -0,0 +1,262 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common";
const Native = VencordNative.pluginHelpers.AppleMusic as PluginNative<typeof import("./native")>;
interface ActivityAssets {
large_image?: string;
large_text?: string;
small_image?: string;
small_text?: string;
}
interface ActivityButton {
label: string;
url: string;
}
interface Activity {
state: string;
details?: string;
timestamps?: {
start?: number;
end?: number;
};
assets?: ActivityAssets;
buttons?: Array<string>;
name: string;
application_id: string;
metadata?: {
button_urls?: Array<string>;
};
type: number;
flags: number;
}
const enum ActivityType {
PLAYING = 0,
LISTENING = 2,
}
const enum ActivityFlag {
INSTANCE = 1 << 0,
}
export interface TrackData {
name: string;
album: string;
artist: string;
appleMusicLink?: string;
songLink?: string;
albumArtwork?: string;
artistArtwork?: string;
playerPosition: number;
duration: number;
}
const enum AssetImageType {
Album = "Album",
Artist = "Artist",
Disabled = "Disabled"
}
const applicationId = "1239490006054207550";
function setActivity(activity: Activity | null) {
FluxDispatcher.dispatch({
type: "LOCAL_ACTIVITY_UPDATE",
activity,
socketId: "AppleMusic",
});
}
const settings = definePluginSettings({
activityType: {
type: OptionType.SELECT,
description: "Which type of activity",
options: [
{ label: "Playing", value: ActivityType.PLAYING, default: true },
{ label: "Listening", value: ActivityType.LISTENING }
],
},
refreshInterval: {
type: OptionType.SLIDER,
description: "The interval between activity refreshes (seconds)",
markers: [1, 2, 2.5, 3, 5, 10, 15],
default: 5,
restartNeeded: true,
},
enableTimestamps: {
type: OptionType.BOOLEAN,
description: "Whether or not to enable timestamps",
default: true,
},
enableButtons: {
type: OptionType.BOOLEAN,
description: "Whether or not to enable buttons",
default: true,
},
nameString: {
type: OptionType.STRING,
description: "Activity name format string",
default: "Apple Music"
},
detailsString: {
type: OptionType.STRING,
description: "Activity details format string",
default: "{name}"
},
stateString: {
type: OptionType.STRING,
description: "Activity state format string",
default: "{artist}"
},
largeImageType: {
type: OptionType.SELECT,
description: "Activity assets large image type",
options: [
{ label: "Album artwork", value: AssetImageType.Album, default: true },
{ label: "Artist artwork", value: AssetImageType.Artist },
{ label: "Disabled", value: AssetImageType.Disabled }
],
},
largeTextString: {
type: OptionType.STRING,
description: "Activity assets large text format string",
default: "{album}"
},
smallImageType: {
type: OptionType.SELECT,
description: "Activity assets small image type",
options: [
{ label: "Album artwork", value: AssetImageType.Album },
{ label: "Artist artwork", value: AssetImageType.Artist, default: true },
{ label: "Disabled", value: AssetImageType.Disabled }
],
},
smallTextString: {
type: OptionType.STRING,
description: "Activity assets small text format string",
default: "{artist}"
},
});
function customFormat(formatStr: string, data: TrackData) {
return formatStr
.replaceAll("{name}", data.name)
.replaceAll("{album}", data.album)
.replaceAll("{artist}", data.artist);
}
function getImageAsset(type: AssetImageType, data: TrackData) {
const source = type === AssetImageType.Album
? data.albumArtwork
: data.artistArtwork;
if (!source) return undefined;
return ApplicationAssetUtils.fetchAssetIds(applicationId, [source]).then(ids => ids[0]);
}
export default definePlugin({
name: "AppleMusicRichPresence",
description: "Discord rich presence for your Apple Music!",
authors: [Devs.RyanCaoDev],
hidden: !navigator.platform.startsWith("Mac"),
reporterTestable: ReporterTestable.None,
settingsAboutComponent() {
return <>
<Forms.FormText>
For the customizable activity format strings, you can use several special strings to include track data in activities!{" "}
<code>{"{name}"}</code> is replaced with the track name; <code>{"{artist}"}</code> is replaced with the artist(s)' name(s); and <code>{"{album}"}</code> is replaced with the album name.
</Forms.FormText>
</>;
},
settings,
start() {
this.updatePresence();
this.updateInterval = setInterval(() => { this.updatePresence(); }, settings.store.refreshInterval * 1000);
},
stop() {
clearInterval(this.updateInterval);
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null });
},
updatePresence() {
this.getActivity().then(activity => { setActivity(activity); });
},
async getActivity(): Promise<Activity | null> {
const trackData = await Native.fetchTrackData();
if (!trackData) return null;
const [largeImageAsset, smallImageAsset] = await Promise.all([
getImageAsset(settings.store.largeImageType, trackData),
getImageAsset(settings.store.smallImageType, trackData)
]);
const assets: ActivityAssets = {};
if (settings.store.largeImageType !== AssetImageType.Disabled) {
assets.large_image = largeImageAsset;
assets.large_text = customFormat(settings.store.largeTextString, trackData);
}
if (settings.store.smallImageType !== AssetImageType.Disabled) {
assets.small_image = smallImageAsset;
assets.small_text = customFormat(settings.store.smallTextString, trackData);
}
const buttons: ActivityButton[] = [];
if (settings.store.enableButtons) {
if (trackData.appleMusicLink)
buttons.push({
label: "Listen on Apple Music",
url: trackData.appleMusicLink,
});
if (trackData.songLink)
buttons.push({
label: "View on SongLink",
url: trackData.songLink,
});
}
return {
application_id: applicationId,
name: customFormat(settings.store.nameString, trackData),
details: customFormat(settings.store.detailsString, trackData),
state: customFormat(settings.store.stateString, trackData),
timestamps: (settings.store.enableTimestamps ? {
start: Date.now() - (trackData.playerPosition * 1000),
end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),
} : undefined),
assets,
buttons: buttons.length ? buttons.map(v => v.label) : undefined,
metadata: { button_urls: buttons.map(v => v.url) || undefined, },
type: settings.store.activityType,
flags: ActivityFlag.INSTANCE,
};
}
});

View file

@ -0,0 +1,120 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { execFile } from "child_process";
import { promisify } from "util";
import type { TrackData } from ".";
const exec = promisify(execFile);
// function exec(file: string, args: string[] = []) {
// return new Promise<{ code: number | null, stdout: string | null, stderr: string | null; }>((resolve, reject) => {
// const process = spawn(file, args, { stdio: [null, "pipe", "pipe"] });
// let stdout: string | null = null;
// process.stdout.on("data", (chunk: string) => { stdout ??= ""; stdout += chunk; });
// let stderr: string | null = null;
// process.stderr.on("data", (chunk: string) => { stdout ??= ""; stderr += chunk; });
// process.on("exit", code => { resolve({ code, stdout, stderr }); });
// process.on("error", err => reject(err));
// });
// }
async function applescript(cmds: string[]) {
const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat());
return stdout;
}
function makeSearchUrl(type: string, query: string) {
const url = new URL("https://tools.applemediaservices.com/api/apple-media/music/US/search.json");
url.searchParams.set("types", type);
url.searchParams.set("limit", "1");
url.searchParams.set("term", query);
return url;
}
const requestOptions: RequestInit = {
headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; rv:125.0) Gecko/20100101 Firefox/125.0" },
};
interface RemoteData {
appleMusicLink?: string,
songLink?: string,
albumArtwork?: string,
artistArtwork?: string;
}
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
async function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) {
if (id === cachedRemoteData?.id) {
if ("data" in cachedRemoteData) return cachedRemoteData.data;
if ("failures" in cachedRemoteData && cachedRemoteData.failures >= 5) return null;
}
try {
const [songData, artistData] = await Promise.all([
fetch(makeSearchUrl("songs", artist + " " + album + " " + name), requestOptions).then(r => r.json()),
fetch(makeSearchUrl("artists", artist.split(/ *[,&] */)[0]), requestOptions).then(r => r.json())
]);
const appleMusicLink = songData?.songs?.data[0]?.attributes.url;
const songLink = songData?.songs?.data[0]?.id ? `https://song.link/i/${songData?.songs?.data[0]?.id}` : undefined;
const albumArtwork = songData?.songs?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
const artistArtwork = artistData?.artists?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
cachedRemoteData = {
id,
data: { appleMusicLink, songLink, albumArtwork, artistArtwork }
};
return cachedRemoteData.data;
} catch (e) {
console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e);
cachedRemoteData = {
id,
failures: (id === cachedRemoteData?.id && "failures" in cachedRemoteData ? cachedRemoteData.failures : 0) + 1
};
return null;
}
}
export async function fetchTrackData(): Promise<TrackData | null> {
try {
await exec("pgrep", ["^Music$"]);
} catch (error) {
return null;
}
const playerState = await applescript(['tell application "Music"', "get player state", "end tell"])
.then(out => out.trim());
if (playerState !== "playing") return null;
const playerPosition = await applescript(['tell application "Music"', "get player position", "end tell"])
.then(text => Number.parseFloat(text.trim()));
const stdout = await applescript([
'set output to ""',
'tell application "Music"',
"set t_id to database id of current track",
"set t_name to name of current track",
"set t_album to album of current track",
"set t_artist to artist of current track",
"set t_duration to duration of current track",
'set output to "" & t_id & "\\n" & t_name & "\\n" & t_album & "\\n" & t_artist & "\\n" & t_duration',
"end tell",
"return output"
]);
const [id, name, album, artist, durationStr] = stdout.split("\n").filter(k => !!k);
const duration = Number.parseFloat(durationStr);
const remoteData = await fetchRemoteData({ id, name, artist, album });
return { name, album, artist, playerPosition, duration, ...remoteData };
}

View file

@ -19,11 +19,11 @@
import { popNotice, showNotice } from "@api/Notices";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import definePlugin, { ReporterTestable } from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
const RpcUtils = findByPropsLazy("fetchApplicationsRPC", "getRemoteIconURL");
const fetchApplicationsRPC = findByCodeLazy("APPLICATION_RPC(", "Client ID");
async function lookupAsset(applicationId: string, key: string): Promise<string> {
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
@ -32,7 +32,7 @@ async function lookupAsset(applicationId: string, key: string): Promise<string>
const apps: any = {};
async function lookupApp(applicationId: string): Promise<string> {
const socket: any = {};
await RpcUtils.fetchApplicationsRPC(socket, applicationId);
await fetchApplicationsRPC(socket, applicationId);
return socket.application;
}
@ -41,6 +41,7 @@ export default definePlugin({
name: "WebRichPresence (arRPC)",
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
authors: [Devs.Ducko],
reporterTestable: ReporterTestable.None,
settingsAboutComponent: () => (
<>

View file

@ -0,0 +1,5 @@
# AutomodContext
Allows you to jump to the messages surrounding an automod hit
![Visualization](https://github.com/Vendicated/Vencord/assets/61953774/d13740c8-2062-4553-b975-82fd3d6cc08b)

View file

@ -0,0 +1,73 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Text } from "@webpack/common";
const { selectChannel } = findByPropsLazy("selectChannel", "selectVoiceChannel");
function jumpToMessage(channelId: string, messageId: string) {
const guildId = ChannelStore.getChannel(channelId)?.guild_id;
selectChannel({
guildId,
channelId,
messageId,
jumpType: "INSTANT"
});
}
function findChannelId(message: any): string | null {
const { embeds: [embed] } = message;
const channelField = embed.fields.find(({ rawName }) => rawName === "channel_id");
if (!channelField) {
return null;
}
return channelField.rawValue;
}
export default definePlugin({
name: "AutomodContext",
description: "Allows you to jump to the messages surrounding an automod hit.",
authors: [Devs.JohnyTheCarrot],
patches: [
{
find: ".Messages.GUILD_AUTOMOD_REPORT_ISSUES",
replacement: {
match: /\.Messages\.ACTIONS.+?}\)(?=,(\(0.{0,40}\.dot.*?}\)),)/,
replace: (m, dot) => `${m},${dot},$self.renderJumpButton({message:arguments[0].message})`
}
}
],
renderJumpButton: ErrorBoundary.wrap(({ message }: { message: any; }) => {
const channelId = findChannelId(message);
if (!channelId) {
return null;
}
return (
<Button
style={{ padding: "2px 8px" }}
look={Button.Looks.LINK}
size={Button.Sizes.SMALL}
color={Button.Colors.LINK}
onClick={() => jumpToMessage(channelId, message.id)}
>
<Text color="text-link" variant="text-xs/normal">
Jump to Surrounding
</Text>
</Button>
);
}, { noop: true })
});

View file

@ -27,7 +27,7 @@ export default definePlugin({
{
find: "BAN_CONFIRM_TITLE.",
replacement: {
match: /src:\i\("\d+"\)/g,
match: /src:\i\("?\d+"?\)/g,
replace: "src: Vencord.Settings.plugins.BANger.source"
}
}

View file

@ -19,7 +19,7 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
import FolderSideBar from "./FolderSideBar";
@ -30,7 +30,7 @@ enum FolderIconDisplay {
MoreThanOneFolderExpanded
}
const { GuildsTree } = findByPropsLazy("GuildsTree");
const GuildsTree = findLazy(m => m.prototype?.moveNextTo);
const SortedGuildStore = findStoreLazy("SortedGuildStore");
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
@ -112,12 +112,12 @@ export default definePlugin({
replacement: [
// Create the isBetterFolders variable in the GuildsBar component
{
match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/,
replace: ",isBetterFolders"
match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/,
replace: "$&,isBetterFolders"
},
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
{
match: /\[(\i)\]=(\(0,\i\.useStateFromStoresArray\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
match: /\[(\i)\]=(\(0,\i\.\i\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
},
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
@ -139,13 +139,13 @@ export default definePlugin({
},
{
// This is the parent folder component
find: ".MAX_GUILD_FOLDER_NAME_LENGTH,",
find: ".toggleGuildFolderExpand(",
predicate: () => settings.store.sidebar && settings.store.showFolderIcon !== FolderIconDisplay.Always,
replacement: [
{
// Modify the expanded state to instead return the list of expanded folders
match: /(useStateFromStores\).{0,20}=>)(\i\.\i)\.isFolderExpanded\(\i\)/,
replace: (_, rest, ExpandedGuildFolderStore) => `${rest}${ExpandedGuildFolderStore}.getExpandedFolders()`,
match: /(\],\(\)=>)(\i\.\i)\.isFolderExpanded\(\i\)\)/,
replace: (_, rest, ExpandedGuildFolderStore) => `${rest}${ExpandedGuildFolderStore}.getExpandedFolders())`,
},
{
// Modify the expanded prop to use the boolean if the above patch fails, or check if the folder is expanded from the list if it succeeds
@ -196,7 +196,7 @@ export default definePlugin({
]
},
{
find: "APPLICATION_LIBRARY,render",
find: "APPLICATION_LIBRARY,render:",
predicate: () => settings.store.sidebar,
replacement: {
// Render the Better Folders sidebar

View file

@ -36,7 +36,7 @@ export default definePlugin({
{
find: ".Messages.GIF,",
replacement: {
match: /alt:(\i)=(\i\.default\.Messages\.GIF)(?=,[^}]*\}=(\i))/,
match: /alt:(\i)=(\i\.\i\.Messages\.GIF)(?=,[^}]*\}=(\i))/,
replace:
// rename prop so we can always use default value
"alt_$$:$1=$self.altify($3)||$2",

View file

@ -13,7 +13,7 @@ export default definePlugin({
authors: [Devs.Samwich],
patches: [
{
find: ".GIFPickerResultTypes.SEARCH",
find: '"state",{resultType:',
replacement: [{
match: /(?<="state",{resultType:)null/,
replace: '"Favorites"'

View file

@ -1,6 +1,6 @@
# BetterRoleContext
Adds options to copy role color and edit role when right clicking roles in the user profile
Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile
![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326)
![](https://github.com/Vendicated/Vencord/assets/45497981/354220a4-09f3-4c5f-a28e-4b19ca775190)

View file

@ -4,14 +4,19 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStores";
import { ImageIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { getCurrentGuild } from "@utils/discord";
import definePlugin from "@utils/types";
import { getCurrentGuild, openImageModal } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
import { Clipboard, GuildStore, Menu, PermissionStore } from "@webpack/common";
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
const DeveloperMode = getSettingStoreLazy("appearance", "developerMode")!;
function PencilIcon() {
return (
<svg
@ -34,14 +39,39 @@ function AppearanceIcon() {
);
}
const settings = definePluginSettings({
roleIconFileFormat: {
type: OptionType.SELECT,
description: "File format to use when viewing role icons",
options: [
{
label: "png",
value: "png",
default: true
},
{
label: "webp",
value: "webp",
},
{
label: "jpg",
value: "jpg"
}
]
}
});
export default definePlugin({
name: "BetterRoleContext",
description: "Adds options to copy role color / edit role when right clicking roles in the user profile",
authors: [Devs.Ven],
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
authors: [Devs.Ven, Devs.goodbee],
dependencies: ["SettingsStoreAPI"],
settings,
start() {
// DeveloperMode needs to be enabled for the context menu to be shown
TextAndImagesSettingsStores.DeveloperMode.updateSetting(true);
DeveloperMode.updateSetting(true);
},
contextMenus: {
@ -63,6 +93,20 @@ export default definePlugin({
);
}
if (role.icon) {
children.push(
<Menu.MenuItem
id="vc-view-role-icon"
label="View Role Icon"
action={() => {
openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`);
}}
icon={ImageIcon}
/>
);
}
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.push(
<Menu.MenuItem

View file

@ -48,6 +48,7 @@ export default definePlugin({
{
find: ".ADD_ROLE_A11Y_LABEL",
all: true,
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
noWarn: true,
replacement: {
@ -57,6 +58,7 @@ export default definePlugin({
},
{
find: ".roleVerifiedIcon",
all: true,
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
noWarn: true,
replacement: {

View file

@ -77,15 +77,6 @@ export default definePlugin({
replace: "$& $self.renderIcon({ ...arguments[0], DeviceIcon: $1 }), false &&"
}
]
},
{
// Add the ability to change BlobMask's lower badge height
// (it allows changing width so we can mirror that logic)
find: "this.getBadgePositionInterpolation(",
replacement: {
match: /(\i\.animated\.rect,{id:\i,x:48-\(\i\+8\)\+4,y:)28(,width:\i\+8,height:)24,/,
replace: (_, leftPart, rightPart) => `${leftPart} 48 - ((this.props.lowerBadgeHeight ?? 16) + 8) + 4 ${rightPart} (this.props.lowerBadgeHeight ?? 16) + 8,`
}
}
],
@ -153,14 +144,16 @@ export default definePlugin({
<PlatformIcon width={14} height={14} />
</div>
}
lowerBadgeWidth={20}
lowerBadgeHeight={20}
lowerBadgeSize={{
width: 20,
height: 20
}}
>
<div
className={SessionIconClasses.sessionIcon}
style={{ backgroundColor: GetOsColor(session.client_info.os) }}
>
<DeviceIcon width={28} height={28} />
<DeviceIcon width={28} height={28} color="currentColor" />
</div>
</BlobMask>
);

View file

@ -83,19 +83,19 @@ export default definePlugin({
find: "this.renderArtisanalHack()",
replacement: [
{ // Fade in on layer
match: /(?<=\((\i),"contextType",\i\.AccessibilityPreferencesContext\);)/,
match: /(?<=\((\i),"contextType",\i\.\i\);)/,
replace: "$1=$self.Layer;",
predicate: () => settings.store.disableFade
},
{ // Lazy-load contents
match: /createPromise:\(\)=>([^:}]*?),webpackId:"\d+",name:(?!="CollectiblesShop")"[^"]+"/g,
match: /createPromise:\(\)=>([^:}]*?),webpackId:"?\d+"?,name:(?!="CollectiblesShop")"[^"]+"/g,
replace: "$&,_:$1",
predicate: () => settings.store.eagerLoad
}
]
},
{ // For some reason standardSidebarView also has a small fade-in
find: "DefaultCustomContentScroller:function()",
find: 'minimal:"contentColumnMinimal"',
replacement: [
{
match: /\(0,\i\.useTransition\)\((\i)/,
@ -111,16 +111,16 @@ export default definePlugin({
{ // Load menu TOC eagerly
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
replacement: {
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
replace: "(async ()=>$2)(),"
match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?null!=\i&&.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/,
replace: "$&(async ()=>$2)(),"
},
predicate: () => settings.store.eagerLoad
},
{ // Settings cog context menu
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
match: /\(0,\i.useDefaultUserSettingsSections\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$self.wrapMenu($&)"
match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$1$self.wrapMenu($2)"
}
}
],

View file

@ -11,7 +11,7 @@ import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, useStateFromStores } from "@webpack/common";
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
@ -30,7 +30,7 @@ function onPickColor(color: number) {
updateColorVars(hexColor);
}
const { saveClientTheme } = findByPropsLazy("saveClientTheme");
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE",settings:{useSystemTheme:"system"===');
function setTheme(theme: string) {
saveClientTheme({ theme });

View file

@ -34,9 +34,9 @@ export default definePlugin({
{
find: ".AVATAR_STATUS_MOBILE_16;",
replacement: {
match: /(?<=fromIsMobile:\i=!0,.+?)status:(\i)/,
match: /(fromIsMobile:\i=!0,.+?)status:(\i)/,
// Rename field to force it to always use "online"
replace: 'status_$:$1="online"'
replace: '$1status_$:$2="online"'
}
}
]

View file

@ -17,24 +17,34 @@
*/
import { Devs } from "@utils/constants";
import { getCurrentChannel, getCurrentGuild } from "@utils/discord";
import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy";
import { relaunch } from "@utils/native";
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
import definePlugin from "@utils/types";
import definePlugin, { PluginNative, StartAt } from "@utils/types";
import * as Webpack from "@webpack";
import { extract, filters, findAll, search } from "@webpack";
import { React, ReactDOM } from "@webpack/common";
import { extract, filters, findAll, findModuleId, search } from "@webpack";
import * as Common from "@webpack/common";
import { loadLazyChunks } from "debug/loadLazyChunks";
import type { ComponentType } from "react";
const WEB_ONLY = (f: string) => () => {
const DESKTOP_ONLY = (f: string) => () => {
throw new Error(`'${f}' is Discord Desktop only.`);
};
export default definePlugin({
name: "ConsoleShortcuts",
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
authors: [Devs.Ven],
const define: typeof Object.defineProperty =
(obj, prop, desc) => {
if (Object.hasOwn(desc, "value"))
desc.writable = true;
getShortcuts() {
return Object.defineProperty(obj, prop, {
configurable: true,
enumerable: true,
...desc
});
};
function makeShortcuts() {
function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) {
const cache = new Map<string, unknown>();
@ -64,16 +74,18 @@ export default definePlugin({
let fakeRenderWin: WeakRef<Window> | undefined;
const find = newFindWrapper(f => f);
const findByProps = newFindWrapper(filters.byProps);
return {
...Vencord.Webpack.Common,
wp: Vencord.Webpack,
wpc: Webpack.wreq.c,
wreq: Webpack.wreq,
...Object.fromEntries(Object.keys(Common).map(key => [key, { getter: () => Common[key] }])),
wp: Webpack,
wpc: { getter: () => Webpack.cache },
wreq: { getter: () => Webpack.wreq },
wpsearch: search,
wpex: extract,
wpexs: (code: string) => extract(Webpack.findModuleId(code)!),
wpexs: (code: string) => extract(findModuleId(code)!),
loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); },
find,
findAll,
findAll: findAll,
findByProps,
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
findByCode: newFindWrapper(filters.byCode),
@ -82,18 +94,21 @@ export default definePlugin({
findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)),
findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]],
findStore: newFindWrapper(filters.byStoreName),
PluginsApi: Vencord.Plugins,
plugins: Vencord.Plugins.plugins,
Settings: Vencord.Settings,
Api: Vencord.Api,
PluginsApi: { getter: () => Vencord.Plugins },
plugins: { getter: () => Vencord.Plugins.plugins },
Settings: { getter: () => Vencord.Settings },
Api: { getter: () => Vencord.Api },
Util: { getter: () => Vencord.Util },
reload: () => location.reload(),
restart: IS_WEB ? WEB_ONLY("restart") : relaunch,
restart: IS_WEB ? DESKTOP_ONLY("restart") : relaunch,
canonicalizeMatch,
canonicalizeReplace,
canonicalizeReplacement,
fakeRender: (component: ComponentType, props: any) => {
const prevWin = fakeRenderWin?.deref();
const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!;
const win = prevWin?.closed === false
? prevWin
: window.open("about:blank", "Fake Render", "popup,width=500,height=500")!;
fakeRenderWin = new WeakRef(win);
win.focus();
@ -115,21 +130,96 @@ export default definePlugin({
});
}
ReactDOM.render(React.createElement(component, props), doc.body.appendChild(document.createElement("div")));
}
};
Common.ReactDOM.render(Common.React.createElement(component, props), doc.body.appendChild(document.createElement("div")));
},
preEnable: (plugin: string) => (Vencord.Settings.plugins[plugin] ??= { enabled: true }).enabled = true,
channel: { getter: () => getCurrentChannel(), preload: false },
channelId: { getter: () => Common.SelectedChannelStore.getChannelId(), preload: false },
guild: { getter: () => getCurrentGuild(), preload: false },
guildId: { getter: () => Common.SelectedGuildStore.getGuildId(), preload: false },
me: { getter: () => Common.UserStore.getCurrentUser(), preload: false },
meId: { getter: () => Common.UserStore.getCurrentUser().id, preload: false },
messages: { getter: () => Common.MessageStore.getMessages(Common.SelectedChannelStore.getChannelId()), preload: false },
Stores: {
getter: () => Object.fromEntries(
Common.Flux.Store.getAll()
.map(store => [store.getName(), store] as const)
.filter(([name]) => name.length > 1)
)
}
};
}
function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
const currentVal = val.getter();
if (!currentVal || val.preload === false) return currentVal;
const value = currentVal[SYM_LAZY_GET]
? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED]
: currentVal;
if (value) define(window.shortcutList, key, { value });
return value;
}
export default definePlugin({
name: "ConsoleShortcuts",
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
authors: [Devs.Ven],
startAt: StartAt.Init,
start() {
const shortcuts = this.getShortcuts();
window.shortcutList = shortcuts;
for (const [key, val] of Object.entries(shortcuts))
const shortcuts = makeShortcuts();
window.shortcutList = {};
for (const [key, val] of Object.entries(shortcuts)) {
if ("getter" in val) {
define(window.shortcutList, key, {
get: () => loadAndCacheShortcut(key, val, true)
});
define(window, key, {
get: () => window.shortcutList[key]
});
} else {
window.shortcutList[key] = val;
window[key] = val;
}
}
// unproxy loaded modules
Webpack.onceReady.then(() => {
setTimeout(() => this.eagerLoad(false), 1000);
if (!IS_WEB) {
const Native = VencordNative.pluginHelpers.ConsoleShortcuts as PluginNative<typeof import("./native")>;
Native.initDevtoolsOpenEagerLoad();
}
});
},
async eagerLoad(forceLoad: boolean) {
await Webpack.onceReady;
const shortcuts = makeShortcuts();
for (const [key, val] of Object.entries(shortcuts)) {
if (!Object.hasOwn(val, "getter") || (val as any).preload === false) continue;
try {
loadAndCacheShortcut(key, val, forceLoad);
} catch { } // swallow not found errors in DEV
}
},
stop() {
delete window.shortcutList;
for (const key in this.getShortcuts())
for (const key in makeShortcuts()) {
delete window[key];
}
}
});

View file

@ -0,0 +1,16 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { IpcMainInvokeEvent } from "electron";
export function initDevtoolsOpenEagerLoad(e: IpcMainInvokeEvent) {
const handleDevtoolsOpened = () => e.sender.executeJavaScript("Vencord.Plugins.plugins.ConsoleShortcuts.eagerLoad(true)");
if (e.sender.isDevToolsOpened())
handleDevtoolsOpened();
else
e.sender.once("devtools-opened", () => handleDevtoolsOpened());
}

View file

@ -0,0 +1,5 @@
# CopyEmojiMarkdown
Allows you to copy emojis as formatted string. Custom emojis will be copied as `<:trolley:1024751352028602449>`, default emojis as `🛒`
![](https://github.com/Vendicated/Vencord/assets/45497981/417f345a-7031-4fe7-8e42-e238870cd547)

View file

@ -0,0 +1,75 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Menu } from "@webpack/common";
const { convertNameToSurrogate } = findByPropsLazy("convertNameToSurrogate");
interface Emoji {
type: string;
id: string;
name: string;
}
interface Target {
dataset: Emoji;
firstChild: HTMLImageElement;
}
function getEmojiMarkdown(target: Target, copyUnicode: boolean): string {
const { id: emojiId, name: emojiName } = target.dataset;
if (!emojiId) {
return copyUnicode
? convertNameToSurrogate(emojiName)
: `:${emojiName}:`;
}
const extension = target?.firstChild.src.match(
/https:\/\/cdn\.discordapp\.com\/emojis\/\d+\.(\w+)/
)?.[1];
return `<${extension === "gif" ? "a" : ""}:${emojiName.replace(/~\d+$/, "")}:${emojiId}>`;
}
const settings = definePluginSettings({
copyUnicode: {
type: OptionType.BOOLEAN,
description: "Copy the raw unicode character instead of :name: for default emojis (👽)",
default: true,
},
});
export default definePlugin({
name: "CopyEmojiMarkdown",
description: "Allows you to copy emojis as formatted string (<:blobcatcozy:1026533070955872337>)",
authors: [Devs.HappyEnderman, Devs.Vishnya],
settings,
contextMenus: {
"expression-picker"(children, { target }: { target: Target }) {
if (target.dataset.type !== "emoji") return;
children.push(
<Menu.MenuItem
id="vc-copy-emoji-markdown"
label="Copy Emoji Markdown"
action={() => {
copyWithToast(
getEmojiMarkdown(target, settings.store.copyUnicode),
"Success! Copied emoji markdown."
);
}}
/>
);
},
},
});

View file

@ -24,20 +24,19 @@ import { closeAllModals } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { maybePromptToUpdate } from "@utils/updater";
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
import { DraftType, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
import { DraftType, ExpressionPickerStore, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
const CrashHandlerLogger = new Logger("CrashHandler");
const { ModalStack, DraftManager, closeExpressionPicker } = proxyLazyWebpack(() => {
const [ModalStack, DraftManager, ExpressionManager] = findBulk(
const { ModalStack, DraftManager } = proxyLazyWebpack(() => {
const [ModalStack, DraftManager] = findBulk(
filters.byProps("pushLazy", "popAll"),
filters.byProps("clearDraft", "saveDraft"),
filters.byProps("closeExpressionPicker", "openExpressionPicker"),);
);
return {
ModalStack,
DraftManager,
closeExpressionPicker: ExpressionManager?.closeExpressionPicker,
DraftManager
};
});
@ -144,7 +143,7 @@ export default definePlugin({
CrashHandlerLogger.debug("Failed to clear drafts.", err);
}
try {
closeExpressionPicker();
ExpressionPickerStore.closeExpressionPicker();
}
catch (err) {
CrashHandlerLogger.debug("Failed to close expression picker.", err);

View file

@ -18,7 +18,7 @@ export default definePlugin({
type: OptionType.SELECT,
options: [
{
label: "Ctrl+Enter (Enter or Shift+Enter for new line)",
label: "Ctrl+Enter (Enter or Shift+Enter for new line) (cmd+enter on macOS)",
value: "ctrl+enter"
},
{
@ -40,9 +40,9 @@ export default definePlugin({
}),
patches: [
{
find: "KeyboardKeys.ENTER&&(!",
find: ".ENTER&&(!",
replacement: {
match: /(?<=(\i)\.which===\i\.KeyboardKeys.ENTER&&).{0,100}(\(0,\i\.hasOpenPlainTextCodeBlock\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
match: /(?<=(\i)\.which===\i\.\i.ENTER&&).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
replace: "$self.shouldSubmit($1, $2)"
}
}
@ -54,7 +54,7 @@ export default definePlugin({
result = event.shiftKey;
break;
case "ctrl+enter":
result = event.ctrlKey;
result = navigator.platform.includes("Mac") ? event.metaKey : event.ctrlKey;
break;
case "enter":
result = !event.shiftKey && !event.ctrlKey;

View file

@ -17,6 +17,7 @@
*/
import { definePluginSettings, Settings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStores";
import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
@ -26,12 +27,15 @@ import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const ShowCurrentGame = getSettingStoreLazy<boolean>("status", "showCurrentGame")!;
async function getApplicationAsset(key: string): Promise<string> {
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
@ -178,7 +182,7 @@ const settings = definePluginSettings({
},
startTime: {
type: OptionType.NUMBER,
description: "Start timestamp in milisecond (only for custom timestamp mode)",
description: "Start timestamp in milliseconds (only for custom timestamp mode)",
onChange: onChange,
disabled: isTimestampDisabled,
isValid: (value: number) => {
@ -188,7 +192,7 @@ const settings = definePluginSettings({
},
endTime: {
type: OptionType.NUMBER,
description: "End timestamp in milisecond (only for custom timestamp mode)",
description: "End timestamp in milliseconds (only for custom timestamp mode)",
onChange: onChange,
disabled: isTimestampDisabled,
isValid: (value: number) => {
@ -390,13 +394,14 @@ export default definePlugin({
name: "CustomRPC",
description: "Allows you to set a custom rich presence.",
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
dependencies: ["SettingsStoreAPI"],
start: setRpc,
stop: () => setRpc(true),
settings,
settingsAboutComponent: () => {
const activity = useAwaiter(createActivity);
const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
const gameActivityEnabled = ShowCurrentGame.useSetting();
const { profileThemeStyle } = useProfileThemeStyle({});
return (
@ -412,7 +417,7 @@ export default definePlugin({
<Button
color={Button.Colors.TRANSPARENT}
className={Margins.top8}
onClick={() => StatusSettingsStores.ShowCurrentGame.updateSetting(true)}
onClick={() => ShowCurrentGame.updateSetting(true)}
>
Enable
</Button>

View file

@ -0,0 +1,5 @@
# CustomIdle
Lets you change the time until your status gets automatically set to idle. You can also prevent idling altogether.
![Plugin Configuration](https://github.com/Vendicated/Vencord/assets/45801973/4e5259b2-18e0-42e5-b69f-efc672ce1e0b)

View file

@ -0,0 +1,94 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Notices } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
const settings = definePluginSettings({
idleTimeout: {
description: "Minutes before Discord goes idle (0 to disable auto-idle)",
type: OptionType.SLIDER,
markers: makeRange(0, 60, 5),
default: 10,
stickToMarkers: false,
restartNeeded: true // Because of the setInterval patch
},
remainInIdle: {
description: "When you come back to Discord, remain idle until you confirm you want to go online",
type: OptionType.BOOLEAN,
default: true
}
});
export default definePlugin({
name: "CustomIdle",
description: "Allows you to set the time before Discord goes idle (or disable auto-idle)",
authors: [Devs.newwares],
settings,
patches: [
{
find: "IDLE_DURATION:function(){return",
replacement: {
match: /(IDLE_DURATION:function\(\){return )\i/,
replace: "$1$self.getIdleTimeout()"
}
},
{
find: 'type:"IDLE",idle:',
replacement: [
{
match: /Math\.min\((\i\.\i\.getSetting\(\)\*\i\.\i\.\i\.SECOND),\i\.\i\)/,
replace: "$1" // Decouple idle from afk (phone notifications will remain at user setting or 10 min maximum)
},
{
match: /\i\.\i\.dispatch\({type:"IDLE",idle:!1}\)/,
replace: "$self.handleOnline()"
},
{
match: /(setInterval\(\i,\.25\*)\i\.\i/,
replace: "$1$self.getIntervalDelay()" // For web installs
}
]
}
],
getIntervalDelay() {
return Math.min(6e5, this.getIdleTimeout());
},
handleOnline() {
if (!settings.store.remainInIdle) {
FluxDispatcher.dispatch({
type: "IDLE",
idle: false
});
return;
}
const backOnlineMessage = "Welcome back! Click the button to go online. Click the X to stay idle until reload.";
if (
Notices.currentNotice?.[1] === backOnlineMessage ||
Notices.noticesQueue.some(([, noticeMessage]) => noticeMessage === backOnlineMessage)
) return;
Notices.showNotice(backOnlineMessage, "Exit idle", () => {
Notices.popNotice();
FluxDispatcher.dispatch({
type: "IDLE",
idle: false
});
});
},
getIdleTimeout() { // milliseconds, default is 6e5
const { idleTimeout } = settings.store;
return idleTimeout === 0 ? Infinity : idleTimeout * 60000;
}
});

View file

@ -69,7 +69,7 @@ async function embedDidMount(this: Component<Props>) {
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
embed.rawTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");
}
if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
@ -182,8 +182,8 @@ export default definePlugin({
// add dearrow button
{
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
replace: "children:[$self.renderButton(this),",
match: /children:\[(?=null!=\i\?(\i)\.renderSuppressButton)/,
replace: "children:[$self.renderButton($1),",
predicate: () => !settings.store.hideButton
}
]

View file

@ -9,7 +9,6 @@ import "./ui/styles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { UserStore } from "@webpack/common";
import { CDN_URL, RAW_SKU_ID, SKU_ID } from "./lib/constants";
@ -20,7 +19,6 @@ import { settings } from "./settings";
import { setDecorationGridDecoration, setDecorationGridItem } from "./ui/components";
import DecorSection from "./ui/components/DecorSection";
const { isAnimatedAvatarDecoration } = findByPropsLazy("isAnimatedAvatarDecoration");
export interface AvatarDecoration {
asset: string;
skuId: string;
@ -61,7 +59,7 @@ export default definePlugin({
},
// Remove NEW label from decor avatar decorations
{
match: /(?<=\.Section\.PREMIUM_PURCHASE&&\i)(?<=avatarDecoration:(\i).+?)/,
match: /(?<=\.\i\.PREMIUM_PURCHASE&&\i)(?<=avatarDecoration:(\i).+?)/,
replace: "||$1.skuId===$self.SKU_ID"
}
]
@ -93,7 +91,7 @@ export default definePlugin({
replacement: [
// Use Decor avatar decoration hook
{
match: /(?<=getAvatarDecorationURL\)\({avatarDecoration:)(\i).avatarDecoration(?=,)/,
match: /(?<=\i\)\({avatarDecoration:)(\i).avatarDecoration(?=,)/,
replace: "$self.useUserDecorAvatarDecoration($1)??$&"
}
]
@ -133,7 +131,7 @@ export default definePlugin({
if (avatarDecoration?.skuId === SKU_ID) {
const parts = avatarDecoration.asset.split("_");
// Remove a_ prefix if it's animated and animation is disabled
if (isAnimatedAvatarDecoration(avatarDecoration.asset) && !canAnimate) parts.shift();
if (avatarDecoration.asset.startsWith("a_") && !canAnimate) parts.shift();
return `${CDN_URL}/${parts.join("_")}.png`;
} else if (avatarDecoration?.skuId === RAW_SKU_ID) {
return avatarDecoration.asset;

View file

@ -9,7 +9,6 @@ import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common";
import type { StateStorage } from "zustand/middleware";
import { AUTHORIZE_URL, CLIENT_ID } from "../constants";
@ -23,7 +22,7 @@ interface AuthorizationState {
isAuthorized: () => boolean;
}
const indexedDBStorage: StateStorage = {
const indexedDBStorage = {
async getItem(name: string): Promise<string | null> {
return DataStore.get(name).then(v => v ?? null);
},
@ -36,9 +35,9 @@ const indexedDBStorage: StateStorage = {
};
// TODO: Move switching accounts subscription inside the store?
export const useAuthorizationStore = proxyLazy(() => zustandCreate<AuthorizationState>(
export const useAuthorizationStore = proxyLazy(() => zustandCreate(
zustandPersist(
(set, get) => ({
(set: any, get: any) => ({
token: null,
tokens: {},
init: () => { set({ token: get().tokens[UserStore.getCurrentUser().id] ?? null }); },
@ -91,7 +90,7 @@ export const useAuthorizationStore = proxyLazy(() => zustandCreate<Authorization
));
},
isAuthorized: () => !!get().token,
}),
} as AuthorizationState),
{
name: "decor-auth",
getStorage: () => indexedDBStorage,

View file

@ -21,7 +21,7 @@ interface UserDecorationsState {
clear: () => void;
}
export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate<UserDecorationsState>((set, get) => ({
export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({
decorations: [],
selectedDecoration: null,
async fetch() {
@ -53,4 +53,4 @@ export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate<User
useUsersDecorationsStore.getState().set(UserStore.getCurrentUser().id, decoration ? decorationToAsset(decoration) : null);
},
clear: () => set({ decorations: [], selectedDecoration: null })
})));
} as UserDecorationsState)));

View file

@ -30,7 +30,7 @@ interface UsersDecorationsState {
set: (userId: string, decoration: string | null) => void;
}
export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecorationsState>((set, get) => ({
export const useUsersDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({
usersDecorations: new Map<string, UserDecorationData>(),
fetchQueue: new Set(),
bulkFetch: debounce(async () => {
@ -40,7 +40,7 @@ export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecor
set({ fetchQueue: new Set() });
const fetchIds = Array.from(fetchQueue);
const fetchIds = [...fetchQueue];
const fetchedUsersDecorations = await getUsersDecorations(fetchIds);
const newUsersDecorations = new Map(usersDecorations);
@ -92,7 +92,7 @@ export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecor
newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() });
set({ usersDecorations: newUsersDecorations });
}
})));
} as UsersDecorationsState)));
export function useUserDecorAvatarDecoration(user?: User): AvatarDecoration | null | undefined {
const [decorAvatarDecoration, setDecorAvatarDecoration] = useState<string | null>(user ? useUsersDecorationsStore.getState().getAsset(user.id) ?? null : null);

View file

@ -15,7 +15,7 @@ import { openChangeDecorationModal } from "../modals/ChangeDecorationModal";
const CustomizationSection = findByCodeLazy(".customizationSectionBackground");
interface DecorSectionProps {
export interface DecorSectionProps {
hideTitle?: boolean;
hideDivider?: boolean;
noMargin?: boolean;

View file

@ -10,5 +10,5 @@ import { extractAndLoadChunksLazy, findByPropsLazy } from "@webpack";
export const cl = classNameFactory("vc-decor-");
export const DecorationModalStyles = findByPropsLazy("modalFooterShopButton");
export const requireAvatarDecorationModal = extractAndLoadChunksLazy(["openAvatarDecorationModal:"]);
export const requireAvatarDecorationModal = extractAndLoadChunksLazy([".COLLECTIBLES_SHOP_FULLSCREEN&&"]);
export const requireCreateStickerModal = extractAndLoadChunksLazy(["stickerInspected]:"]);

View file

@ -9,7 +9,7 @@ import { Link } from "@components/Link";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { filters, findComponentByCodeLazy, mapMangledModuleLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common";
import { GUILD_ID, INVITE_KEY, RAW_SKU_ID } from "../../lib/constants";
@ -19,7 +19,10 @@ import { AvatarDecorationModalPreview } from "../components";
const FileUpload = findComponentByCodeLazy("fileUploadInput,");
const { default: HelpMessage, HelpMessageTypes } = findByPropsLazy("HelpMessageTypes");
const { HelpMessage, HelpMessageTypes } = mapMangledModuleLazy('POSITIVE=3]="POSITIVE', {
HelpMessageTypes: filters.byProps("POSITIVE", "WARNING"),
HelpMessage: filters.byCode(".iconDiv")
});
function useObjectURL(object: Blob | MediaSource | null) {
const [url, setUrl] = useState<string | null>(null);

View file

@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types";
import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { filters, findAll, search } from "@webpack";
const PORT = 8485;
@ -243,6 +243,7 @@ export default definePlugin({
name: "DevCompanion",
description: "Dev Companion Plugin",
authors: [Devs.Ven],
reporterTestable: ReporterTestable.None,
settings,
toolboxActions: {

View file

@ -29,7 +29,7 @@ export default definePlugin({
{
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
replacement: {
match: /,?(?=\i\(this,"idleTimeout",new \i\.Timeout\))/,
match: /,?(?=\i\(this,"idleTimeout",new \i\.\i\))/,
replace: ";return;"
}
},

View file

@ -0,0 +1,35 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { moment } from "@webpack/common";
export default definePlugin({
name: "DontRoundMyTimestamps",
authors: [Devs.Lexi],
description: "Always rounds relative timestamps down, so 7.6y becomes 7y instead of 8y",
start() {
moment.relativeTimeRounding(Math.floor);
},
stop() {
moment.relativeTimeRounding(Math.round);
}
});

View file

@ -23,12 +23,12 @@ import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
import definePlugin from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { findByCodeLazy, findStoreLazy } from "@webpack";
import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
import { Promisable } from "type-fest";
const StickersStore = findStoreLazy("StickersStore");
const EmojiManager = findByPropsLazy("fetchEmoji", "uploadEmoji", "deleteEmoji");
const uploadEmoji = findByCodeLazy(".GUILD_EMOJIS(", "EMOJI_UPLOAD_START");
interface Sticker {
t: "Sticker";
@ -106,7 +106,7 @@ async function cloneEmoji(guildId: string, emoji: Emoji) {
reader.readAsDataURL(data);
});
return EmojiManager.uploadEmoji({
return uploadEmoji({
guildId,
name: emoji.name.split("~")[0],
image: dataUrl

View file

@ -0,0 +1,3 @@
#staff-help-popout-staff-help-bug-reporter {
display: none;
}

View file

@ -17,22 +17,23 @@
*/
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Forms, React, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
import { Forms, React } from "@webpack/common";
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
import hideBugReport from "./hideBugReport.css?managed";
const KbdStyles = findByPropsLazy("key", "combo");
const settings = definePluginSettings({
enableIsStaff: {
description: "Enable isStaff",
toolbarDevMenu: {
type: OptionType.BOOLEAN,
description: "Change the Help (?) toolbar button (top right in chat) to Discord's developer menu",
default: false,
restartNeeded: true
}
@ -40,7 +41,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "Experiments",
description: "Enable Access to Experiments in Discord!",
description: "Enable Access to Experiments & other dev-only features in Discord!",
authors: [
Devs.Megu,
Devs.Ven,
@ -48,6 +49,7 @@ export default definePlugin({
Devs.BanTheNons,
Devs.Nuckyz
],
settings,
patches: [
@ -65,37 +67,35 @@ export default definePlugin({
replace: "$1=!0;"
}
},
{
find: '"isStaff",',
predicate: () => settings.store.enableIsStaff,
replacement: [
{
match: /(?<=>)(\i)\.hasFlag\((\i\.\i)\.STAFF\)(?=})/,
replace: (_, user, flags) => `$self.isStaff(${user},${flags})`
},
{
match: /hasFreePremium\(\){return this.isStaff\(\)\s*?\|\|/,
replace: "hasFreePremium(){return ",
}
]
},
{
find: 'H1,title:"Experiments"',
replacement: {
match: 'title:"Experiments",children:[',
replace: "$&$self.WarningCard(),"
}
},
// change top right chat toolbar button from the help one to the dev one
{
find: "toolbar:function",
replacement: {
match: /\i\.isStaff\(\)/,
replace: "true"
},
predicate: () => settings.store.toolbarDevMenu
},
// makes the Favourites Server experiment allow favouriting DMs and threads
{
find: "useCanFavoriteChannel",
replacement: {
match: /!\(\i\.isDM\(\)\|\|\i\.isThread\(\)\)/,
replace: "true",
}
}
],
isStaff(user: User, flags: any) {
try {
return UserStore.getCurrentUser()?.id === user.id || user.hasFlag(flags.STAFF);
} catch (err) {
new Logger("Experiments").error(err);
return user.hasFlag(flags.STAFF);
}
},
start: () => enableStyle(hideBugReport),
stop: () => disableStyle(hideBugReport),
settingsAboutComponent: () => {
const isMacOS = navigator.platform.includes("Mac");
@ -105,14 +105,12 @@ export default definePlugin({
<React.Fragment>
<Forms.FormTitle tag="h3">More Information</Forms.FormTitle>
<Forms.FormText variant="text-md/normal">
You can enable client DevTools{" "}
You can open Discord's DevTools via {" "}
<div className={KbdStyles.combo} style={{ display: "inline-flex" }}>
<kbd className={KbdStyles.key}>{modKey}</kbd> +{" "}
<kbd className={KbdStyles.key}>{altKey}</kbd> +{" "}
<kbd className={KbdStyles.key}>O</kbd>{" "}
after enabling <code>isStaff</code> below
</Forms.FormText>
<Forms.FormText>
and then toggling <code>Enable DevTools</code> in the <code>Developer Options</code> tab in settings.
</div>
</Forms.FormText>
</React.Fragment>
);
@ -128,6 +126,12 @@ export default definePlugin({
<Forms.FormText className={Margins.top8}>
Only use experiments if you know what you're doing. Vencord is not responsible for any damage caused by enabling experiments.
If you don't know what an experiment does, ignore it. Do not ask us what experiments do either, we probably don't know.
</Forms.FormText>
<Forms.FormText className={Margins.top8}>
No, you cannot use server-side features like checking the "Send to Client" box.
</Forms.FormText>
</ErrorCard>
), { noop: true })

View file

@ -25,7 +25,7 @@ import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { CustomEmoji } from "@webpack/types";
import type { Emoji } from "@webpack/types";
import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react";
@ -37,8 +37,8 @@ const StickerStore = findStoreLazy("StickersStore") as {
};
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
const RoleSubscriptionEmojiUtils = findByPropsLazy("isUnusableRoleSubscriptionEmoji");
const BINARY_READ_OPTIONS = findByPropsLazy("readerFactory");
function searchProtoClassField(localName: string, protoClass: any) {
const field = protoClass?.fields?.find((field: any) => field.localName === localName);
@ -54,16 +54,22 @@ const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoCla
const enum EmojiIntentions {
REACTION = 0,
STATUS = 1,
COMMUNITY_CONTENT = 2,
CHAT = 3,
GUILD_STICKER_RELATED_EMOJI = 4,
GUILD_ROLE_BENEFIT_EMOJI = 5,
COMMUNITY_CONTENT_ONLY = 6,
SOUNDBOARD = 7
REACTION,
STATUS,
COMMUNITY_CONTENT,
CHAT,
GUILD_STICKER_RELATED_EMOJI,
GUILD_ROLE_BENEFIT_EMOJI,
COMMUNITY_CONTENT_ONLY,
SOUNDBOARD,
VOICE_CHANNEL_TOPIC,
GIFT,
AUTO_SUGGESTION,
POLLS
}
const IS_BYPASSEABLE_INTENTION = `[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`;
const enum StickerType {
PNG = 1,
APNG = 2,
@ -198,39 +204,46 @@ export default definePlugin({
patches: [
{
find: ".PREMIUM_LOCKED;",
group: true,
predicate: () => settings.store.enableEmojiBypass,
replacement: [
{
// Create a variable for the intention of listing the emoji
match: /(?<=,intention:(\i).+?;)/,
replace: (_, intention) => `let fakeNitroIntention=${intention};`
// Create a variable for the intention of using the emoji
match: /(?<=\.USE_EXTERNAL_EMOJIS.+?;)(?<=intention:(\i).+?)/,
replace: (_, intention) => `const fakeNitroIntention=${intention};`
},
{
// Send the intention of listing the emoji to the nitro permission check functions
match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g,
replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0'
// Disallow the emoji for external if the intention doesn't allow it
match: /&&!\i&&!\i(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`
},
{
// Disallow the emoji if the intention doesn't allow it
match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))`
// Disallow the emoji for unavailable if the intention doesn't allow it
match: /!\i\.available(?=\)return \i\.\i\.GUILD_SUBSCRIPTION_UNAVAILABLE;)/,
replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`
},
{
// Make the emoji always available if the intention allows it
match: /if\(!\i\.available/,
replace: m => `${m}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
// Disallow the emoji for premium locked if the intention doesn't allow it
match: /!\i\.\i\.canUseEmojisEverywhere\(\i\)/,
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
},
{
// Allow animated emojis to be used if the intention allows it
match: /(?<=\|\|)\i\.\i\.canUseAnimatedEmojis\(\i\)/,
replace: m => `(${m}||${IS_BYPASSEABLE_INTENTION})`
}
]
},
// Allow emojis and animated emojis to be sent everywhere
{
find: "canUseAnimatedEmojis:function",
predicate: () => settings.store.enableEmojiBypass,
// FIXME
// Allows the usage of subscription-locked emojis
/* {
find: ".getUserIsAdmin(",
replacement: {
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))(?=})/g,
replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
match: /(?=.+?\.getUserIsAdmin\((?<=function (\i)\(\i,\i\){.+?))(\i):function\(\){return \1}/,
// Replace the original export with a func that always returns false and alias the original
replace: "$2:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function(){return $1}"
}
},
}, */
// Allow stickers to be sent everywhere
{
find: "canUseCustomStickersEverywhere:function",
@ -242,10 +255,10 @@ export default definePlugin({
},
// Make stickers always available
{
find: "\"SENDABLE\"",
find: '"SENDABLE"',
predicate: () => settings.store.enableStickerBypass,
replacement: {
match: /(\w+)\.available\?/,
match: /\i\.available\?/,
replace: "true?"
}
},
@ -321,19 +334,19 @@ export default definePlugin({
]
},
{
find: "renderEmbeds(",
find: "}renderEmbeds(",
replacement: [
{
// Call our function to decide whether the embed should be ignored or not
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
match: /(renderEmbeds\((\i)\){)(.+?embeds\.map\((\i)=>{)/,
match: /(renderEmbeds\((\i)\){)(.+?embeds\.map\(\((\i),\i\)?=>{)/,
replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;`
},
{
// Patch the stickers array to add fake nitro stickers
predicate: () => settings.store.transformStickers,
match: /(?<=renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;)/,
replace: (_, message, stickers) => `${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`
match: /renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;/,
replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`
},
{
// Filter attachments to remove fake nitro stickers or emojis
@ -349,7 +362,7 @@ export default definePlugin({
replacement: [
{
// Export the renderable sticker to be used in the fake nitro sticker notice
match: /let{renderableSticker:(\i).{0,250}isGuildSticker.+?channel:\i,/,
match: /let{renderableSticker:(\i).{0,270}sticker:\i,channel:\i,/,
replace: (m, renderableSticker) => `${m}fakeNitroRenderableSticker:${renderableSticker},`
},
{
@ -387,7 +400,7 @@ export default definePlugin({
},
// Separate patch for allowing using custom app icons
{
find: ".FreemiumAppIconIds.DEFAULT&&(",
find: /\.getCurrentDesktopIcon.{0,25}\.isPremium/,
replacement: {
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
replace: "true"
@ -408,15 +421,6 @@ export default definePlugin({
match: /canUseCustomNotificationSounds:function\(\i\){/,
replace: "$&return true;"
}
},
// Allows the usage of subscription-locked emojis
{
find: "isUnusableRoleSubscriptionEmoji:function",
replacement: {
match: /isUnusableRoleSubscriptionEmoji:function/,
// replace the original export with a func that always returns false and alias the original
replace: "isUnusableRoleSubscriptionEmoji:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function"
}
}
],
@ -469,12 +473,12 @@ export default definePlugin({
const premiumType = UserStore?.getCurrentUser()?.premiumType ?? 0;
if (premiumType === 2 || backgroundGradientPresetId == null) return original();
if (!PreloadedUserSettingsActionCreators || !AppearanceSettingsActionCreators || !ClientThemeSettingsActionsCreators || !ProtoUtils) return;
if (!PreloadedUserSettingsActionCreators || !AppearanceSettingsActionCreators || !ClientThemeSettingsActionsCreators || !BINARY_READ_OPTIONS) return;
const currentAppearanceSettings = PreloadedUserSettingsActionCreators.getCurrentValue().appearance;
const newAppearanceProto = currentAppearanceSettings != null
? AppearanceSettingsActionCreators.fromBinary(AppearanceSettingsActionCreators.toBinary(currentAppearanceSettings), ProtoUtils.BINARY_READ_OPTIONS)
? AppearanceSettingsActionCreators.fromBinary(AppearanceSettingsActionCreators.toBinary(currentAppearanceSettings), BINARY_READ_OPTIONS)
: AppearanceSettingsActionCreators.create();
newAppearanceProto.theme = theme;
@ -809,12 +813,13 @@ export default definePlugin({
UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DraftType.ChannelMessage);
},
canUseEmote(e: CustomEmoji, channelId: string) {
if (e.require_colons === false) return true;
canUseEmote(e: Emoji, channelId: string) {
if (e.type === 0) return true;
if (e.available === false) return false;
const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
if (isUnusableRoleSubEmoji(e, this.guildId)) return false;
// FIXME
/* const isUnusableRoleSubEmoji = isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
if (isUnusableRoleSubEmoji(e, this.guildId)) return false; */
if (this.canUseEmotes)
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);

View file

@ -0,0 +1,3 @@
.vc-fpt-preview * {
pointer-events: none;
}

Some files were not shown because too many files have changed in this diff Show more